mirror of
https://github.com/blakeblackshear/frigate.git
synced 2026-09-29 19:36:57 +03:00
Compare commits
20
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
344efb6bc1 | ||
|
|
8e55da67b0 | ||
|
|
62d90e8de8 | ||
|
|
599e0acad7 | ||
|
|
e5382db70e | ||
|
|
4e13c3c9a0 | ||
|
|
c62c31361f | ||
|
|
144513d3d6 | ||
|
|
22c3dfa5a5 | ||
|
|
4e68c4723f | ||
|
|
dbce2d5a43 | ||
|
|
c00ea6a481 | ||
|
|
a4c0aad206 | ||
|
|
6aa2a010ce | ||
|
|
5746f16472 | ||
|
|
24ab9460f5 | ||
|
|
9eb2c841a5 | ||
|
|
e73a14db5d | ||
|
|
4883e20898 | ||
|
|
33c00a27e4 |
@@ -8,6 +8,7 @@ amdgpu
|
||||
analyzeduration
|
||||
Annke
|
||||
apexcharts
|
||||
Aqara
|
||||
arange
|
||||
argmax
|
||||
argmin
|
||||
@@ -64,6 +65,7 @@ dsize
|
||||
dtype
|
||||
ECONNRESET
|
||||
edgetpu
|
||||
Eufy
|
||||
facenet
|
||||
fastapi
|
||||
faststart
|
||||
|
||||
@@ -293,6 +293,10 @@ networking:
|
||||
|
||||
This setting is for advanced users. For the majority of use cases it's recommended to change the `ports` section of your Docker compose file or use the Docker `run` `--publish` option instead, e.g. `-p 443:8971`. Changing Frigate's ports may break some integrations.
|
||||
|
||||
The internal and external ports must be different port numbers, and Frigate will refuse to start otherwise. Requests arriving on the internal port are treated as authenticated admins, so pointing both at the same port would remove authentication from the external one.
|
||||
|
||||
Nginx binds these ports when it starts, so port changes only take effect after Frigate restarts.
|
||||
|
||||
:::
|
||||
|
||||
### Customizing the Nginx configuration
|
||||
|
||||
@@ -256,7 +256,7 @@ The only field that is valid at the camera level is `enabled`.
|
||||
|
||||
#### Live transcription
|
||||
|
||||
The single camera Live view in the Frigate UI supports live transcription of audio for streams defined with the `audio` role. Use the Enable/Disable Live Audio Transcription button/switch to toggle transcription processing. When speech is heard, the UI will display a black box over the top of the camera stream with text. The MQTT topic `frigate/<camera_name>/audio/transcription` will also be updated in real-time with transcribed text.
|
||||
The single camera Live view in the Frigate UI supports live transcription of audio for streams defined with the `audio` role. Use the Enable/Disable Live Audio Transcription button/switch to toggle transcription processing, or toggle it outside of the UI with the [`frigate/<camera_name>/audio_transcription/set`](/integrations/mqtt#frigatecamera_nameaudio_transcriptionset) MQTT topic or the HTTP API. When speech is heard, the UI will display a black box over the top of the camera stream with text. The MQTT topic `frigate/<camera_name>/audio/transcription` will also be updated in real-time with transcribed text.
|
||||
|
||||
Results can be error-prone due to a number of factors, including:
|
||||
|
||||
|
||||
@@ -67,4 +67,6 @@ If your stream won't play, has no audio, uses excessive CPU, or otherwise misbeh
|
||||
|
||||
## Homekit Configuration
|
||||
|
||||
To add camera streams to Homekit Frigate must be configured in docker to use `host` networking mode. Once that is done, you can use the go2rtc WebUI (accessed via port 1984, which is disabled by default) to export a camera to Homekit. Any changes made will automatically be saved to `/config/go2rtc_homekit.yml`.
|
||||
To export camera streams to HomeKit, Frigate must be configured in docker to use `host` networking mode. HomeKit settings are stored in `/config/go2rtc_homekit.yml` rather than in your Frigate config, and are edited through the go2rtc config editor at `http://<frigate_host>:1984/editor.html`. Pairings are saved back to that file automatically.
|
||||
|
||||
See the [HomeKit integration docs](/integrations/homekit) for the full setup, including the video and audio requirements HomeKit places on the stream.
|
||||
|
||||
@@ -297,6 +297,14 @@ detectors:
|
||||
|
||||
:::
|
||||
|
||||
### Intel NPU host requirements {#intel-npu-requirements}
|
||||
|
||||
The NPU firmware is loaded by the host kernel and is not part of the Frigate image. Everything else the NPU needs is bundled in the container, so host NPU libraries should never be mounted in.
|
||||
|
||||
Frigate bundles a specific version of Intel's [linux-npu-driver](https://github.com/intel/linux-npu-driver/releases), and the host firmware must come from that release or a newer one. Firmware older than the bundled driver may fail with `MAPPED_INFERENCE_VERSION is NOT compatible with the ELF`, where `Expected` is the version the firmware supports and `received` is the version the bundled compiler produced. Distributions often package older firmware than the driver Frigate ships, so check the build date on the host with `sudo dmesg | grep -i vpu` and update it there if needed.
|
||||
|
||||
Intel NPUs cannot be used under Home Assistant OS, which does not include the NPU firmware.
|
||||
|
||||
### Configuration {#configuration-openvino}
|
||||
|
||||
<ModelConfigDropdown detectorTitle="OpenVINO" models={objectDetectorsModels.openvino.models} />
|
||||
|
||||
@@ -3,35 +3,100 @@ id: homekit
|
||||
title: HomeKit
|
||||
---
|
||||
|
||||
Frigate cameras can be integrated with Apple HomeKit through go2rtc. This allows you to view your camera streams directly in the Apple Home app on your iOS, iPadOS, macOS, and tvOS devices.
|
||||
Frigate cameras can be exported to Apple HomeKit through go2rtc. Each exported camera appears as an accessory in the Apple Home app on your iOS, iPadOS, macOS, and tvOS devices.
|
||||
|
||||
## Overview
|
||||
|
||||
HomeKit integration is handled entirely through go2rtc, which is embedded in Frigate. go2rtc provides the necessary HomeKit Accessory Protocol (HAP) server to expose your cameras to HomeKit.
|
||||
Exporting cameras is handled entirely through go2rtc, which is embedded in Frigate. go2rtc provides the necessary HomeKit Accessory Protocol (HAP) server, so your camera is published to HomeKit as an accessory in its own right.
|
||||
|
||||
## Setup
|
||||
:::note
|
||||
|
||||
All HomeKit configuration and pairing should be done through the **go2rtc WebUI**.
|
||||
This is the opposite of importing a HomeKit camera. go2rtc can also pair with an existing HomeKit camera (Aqara, Eve, Eufy, and similar) and use it as a stream source, which is what the `add` page of the go2rtc WebUI is for. That page discovers HomeKit accessories on your network and will not list your Frigate cameras. It is not used for exporting.
|
||||
|
||||
### Accessing the go2rtc WebUI
|
||||
|
||||
The go2rtc WebUI is available at:
|
||||
|
||||
```
|
||||
http://<frigate_host>:1984
|
||||
```
|
||||
|
||||
Replace `<frigate_host>` with the IP address or hostname of your Frigate server.
|
||||
|
||||
### Pairing Cameras
|
||||
|
||||
1. Navigate to the go2rtc WebUI at `http://<frigate_host>:1984`
|
||||
2. Use the `add` section to add a new camera to HomeKit
|
||||
3. Follow the on-screen instructions to generate pairing codes for your cameras
|
||||
:::
|
||||
|
||||
## Requirements
|
||||
|
||||
- Frigate must be accessible on your local network using host network_mode
|
||||
- Your iOS device must be on the same network as Frigate
|
||||
- Port 1984 must be accessible for the go2rtc WebUI
|
||||
- For detailed go2rtc configuration options, refer to the [go2rtc documentation](https://github.com/AlexxIT/go2rtc)
|
||||
- Frigate must be running with `network_mode: host` so that HomeKit can discover your cameras over mDNS
|
||||
- Your Apple device must be on the same network as Frigate
|
||||
- Port 1984 must be accessible so you can reach the go2rtc WebUI
|
||||
|
||||
HomeKit also places strict limits on the stream itself. go2rtc passes your stream through without resizing or re-encoding it, so the stream you export must already meet these requirements:
|
||||
|
||||
- **Video:** H.264 at 1920x1080, 1280x720, or 320x240
|
||||
- **Audio:** Opus, mono, 16 kHz
|
||||
|
||||
A camera's full resolution stream usually does not qualify. See [Exporting a compatible stream](#exporting-a-compatible-stream) below.
|
||||
|
||||
## Configuration
|
||||
|
||||
HomeKit settings are stored in `/config/go2rtc_homekit.yml`. This is a separate file from your Frigate config, because go2rtc needs to write your pairings back to it when you pair a device.
|
||||
|
||||
Edit it using the go2rtc config editor, which writes to that file directly:
|
||||
|
||||
```
|
||||
http://<frigate_host>:1984/editor.html
|
||||
```
|
||||
|
||||
Replace `<frigate_host>` with the IP address or hostname of your Frigate server. The editor will be empty until you add a HomeKit section, since this file holds only your HomeKit settings and not the rest of your go2rtc config.
|
||||
|
||||
:::warning
|
||||
|
||||
Do not put the `homekit:` section in the `go2rtc:` section of your Frigate config.
|
||||
|
||||
Frigate regenerates that config on every startup, so go2rtc cannot save your pairings to it. Pairing will appear to succeed and then fail after the next restart with `PairVerify with unknown client_id`. If the section exists in both places, your saved pairings are erased on every restart.
|
||||
|
||||
:::
|
||||
|
||||
Add an entry for each camera you want to export. The key must match the name of a go2rtc stream, and the pin must be 8 digits. This is the number the Home app calls the setup code:
|
||||
|
||||
```yaml
|
||||
homekit:
|
||||
front_door:
|
||||
name: Front Door
|
||||
pin: "12345678"
|
||||
```
|
||||
|
||||
If the key does not match a go2rtc stream, go2rtc logs `[homekit] missing stream:` at startup and the camera will not appear in the Home app.
|
||||
|
||||
:::note
|
||||
|
||||
go2rtc derives each accessory's HomeKit identity from this key, so renaming it later means the camera appears as a new accessory and has to be paired again. Settle on the name before you pair.
|
||||
|
||||
:::
|
||||
|
||||
Frigate keeps only the `homekit:` section of this file when it starts, so do not store streams or other go2rtc settings in it.
|
||||
|
||||
### Exporting a compatible stream
|
||||
|
||||
If a camera's stream does not meet the requirements listed above, define a scaled restream in your Frigate config and point HomeKit at that stream instead of the original:
|
||||
|
||||
```yaml
|
||||
go2rtc:
|
||||
streams:
|
||||
front_door:
|
||||
- rtsp://user:password@192.168.1.50:554/stream
|
||||
front_door_homekit:
|
||||
- "ffmpeg:front_door#video=h264#width=1280#height=720#audio=opus/16000"
|
||||
```
|
||||
|
||||
```yaml
|
||||
# /config/go2rtc_homekit.yml
|
||||
homekit:
|
||||
front_door_homekit:
|
||||
name: Front Door
|
||||
pin: "12345678"
|
||||
```
|
||||
|
||||
Add `#hardware=cuda`, `#hardware=vaapi`, or the appropriate value for your system to transcode using your GPU. Note that NVENC cannot encode H.264 wider than 4096 pixels, so very wide streams must be scaled down as shown above rather than only re-encoded.
|
||||
|
||||
## Pairing Cameras
|
||||
|
||||
1. Restart Frigate after adding the `homekit:` section
|
||||
2. In the Apple Home app, choose **Add Accessory**, then **More options** to enter a code manually
|
||||
3. Select your camera and enter the pin you configured as the setup code
|
||||
4. Confirm that a `pairings:` list now appears under the camera in `/config/go2rtc_homekit.yml`
|
||||
|
||||
Pairings are saved back to that file automatically. If step 4 shows no `pairings:` list, check the Frigate log for `[homekit] can't save`, which means the `homekit:` section is missing from `/config/go2rtc_homekit.yml`.
|
||||
|
||||
For detailed go2rtc configuration options, refer to the [go2rtc documentation](https://github.com/AlexxIT/go2rtc).
|
||||
|
||||
@@ -390,6 +390,18 @@ Topic to turn audio detection for a camera on and off. Expected values are `ON`
|
||||
|
||||
Topic with current state of audio detection for a camera. Published values are `ON` and `OFF`.
|
||||
|
||||
### `frigate/<camera_name>/audio_transcription/set`
|
||||
|
||||
Topic to turn [live audio transcription](/configuration/audio_detectors#live-transcription) for a camera on and off. Expected values are `ON` and `OFF`. Transcribed text is published to `frigate/<camera_name>/audio/transcription`.
|
||||
|
||||
`ON` is ignored unless audio transcription is enabled in the config for the camera. Unlike the other camera toggles, this one is not persisted across Frigate restarts.
|
||||
|
||||
**NOTE:** Requires audio detection and transcription to be enabled
|
||||
|
||||
### `frigate/<camera_name>/audio_transcription/state`
|
||||
|
||||
Topic with current state of live audio transcription for a camera. Published values are `ON` and `OFF`.
|
||||
|
||||
### `frigate/<camera_name>/recordings/set`
|
||||
|
||||
Topic to turn recordings for a camera on and off. Expected values are `ON` and `OFF`. The change is persisted across Frigate restarts (see [Runtime toggle persistence](/configuration/live#runtime-toggle-persistence)).
|
||||
|
||||
+9
-9
@@ -31,7 +31,7 @@ from frigate.api.media_auth import (
|
||||
deny_response_for_media_uri,
|
||||
is_role_restricted,
|
||||
)
|
||||
from frigate.config import AuthConfig, NetworkingConfig, ProxyConfig
|
||||
from frigate.config import AuthConfig, ProxyConfig
|
||||
from frigate.const import CONFIG_DIR, JWT_SECRET_ENV_VAR, PASSWORD_HASH_ALGORITHM
|
||||
from frigate.models import User
|
||||
|
||||
@@ -620,18 +620,18 @@ def resolve_role(
|
||||
def auth(request: Request):
|
||||
auth_config: AuthConfig = request.app.frigate_config.auth
|
||||
proxy_config: ProxyConfig = request.app.frigate_config.proxy
|
||||
networking_config: NetworkingConfig = request.app.frigate_config.networking
|
||||
|
||||
success_response = Response("", status_code=202)
|
||||
|
||||
# handle case where internal port is a string with ip:port
|
||||
internal_port = networking_config.listen.internal
|
||||
if type(internal_port) is str:
|
||||
internal_port = int(internal_port.split(":")[-1])
|
||||
|
||||
# dont require auth if the request is on the internal port
|
||||
# this header is set by Frigate's nginx proxy, so it cant be spoofed
|
||||
if int(request.headers.get("x-server-port", default=0)) == internal_port:
|
||||
# this header is set by Frigate's nginx proxy, so it cant be spoofed.
|
||||
# the port is the boot-time snapshot rather than the live config value:
|
||||
# nginx's listeners are fixed at container start, so an in-memory config
|
||||
# change must never move the port that is trusted here
|
||||
if (
|
||||
int(request.headers.get("x-server-port", default=0))
|
||||
== request.app.auth_internal_port
|
||||
):
|
||||
success_response.headers["remote-user"] = "anonymous"
|
||||
success_response.headers["remote-role"] = "admin"
|
||||
return success_response
|
||||
|
||||
@@ -152,6 +152,8 @@ def create_fastapi_app(
|
||||
app.include_router(debug_replay.router)
|
||||
# App Properties
|
||||
app.frigate_config = frigate_config
|
||||
# snapshot the port nginx bound at startup, the live config can be swapped
|
||||
app.auth_internal_port = frigate_config.networking.listen.internal_port
|
||||
app.genai_manager = GenAIClientManager(frigate_config)
|
||||
app.embeddings = embeddings
|
||||
app.detected_frames_processor = detected_frames_processor
|
||||
|
||||
+16
-1
@@ -53,6 +53,7 @@ from frigate.util.file import (
|
||||
)
|
||||
from frigate.util.image import get_image_from_recording, get_image_quality_params
|
||||
from frigate.util.media import get_keyframe_before
|
||||
from frigate.util.object import create_empty_regions_grid
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -1083,7 +1084,21 @@ def clear_region_grid(request: Request, camera_name: str):
|
||||
status_code=404,
|
||||
)
|
||||
|
||||
Regions.delete().where(Regions.camera == camera_name).execute()
|
||||
# store an empty grid instead of deleting the row so the grid is
|
||||
# rebuilt from newly tracked objects and not from all past history
|
||||
region = {
|
||||
Regions.camera: camera_name,
|
||||
Regions.grid: create_empty_regions_grid(),
|
||||
Regions.last_update: datetime.now().timestamp(),
|
||||
}
|
||||
(
|
||||
Regions.insert(region)
|
||||
.on_conflict(
|
||||
conflict_target=[Regions.camera],
|
||||
update=region,
|
||||
)
|
||||
.execute()
|
||||
)
|
||||
return JSONResponse(
|
||||
content={"success": True, "message": "Region grid cleared"},
|
||||
)
|
||||
|
||||
@@ -182,7 +182,7 @@ async def get_motion_search_status_endpoint(
|
||||
)
|
||||
|
||||
job = get_motion_search_job(job_id)
|
||||
if not job:
|
||||
if not job or job.camera != camera_name:
|
||||
return JSONResponse(
|
||||
content={"success": False, "message": "Job not found"},
|
||||
status_code=404,
|
||||
@@ -253,7 +253,7 @@ async def cancel_motion_search_endpoint(
|
||||
)
|
||||
|
||||
job = get_motion_search_job(job_id)
|
||||
if not job:
|
||||
if not job or job.camera != camera_name:
|
||||
return JSONResponse(
|
||||
content={"success": False, "message": "Job not found"},
|
||||
status_code=404,
|
||||
|
||||
+10
-22
@@ -122,7 +122,6 @@ class FrigateApp:
|
||||
self.ptz_metrics: dict[str, PTZMetrics] = {}
|
||||
self.processes: dict[str, int] = {}
|
||||
self.embeddings: EmbeddingsContext | None = None
|
||||
self.profile_manager: ProfileManager | None = None
|
||||
self.config_holder = ConfigHolder(config)
|
||||
|
||||
@property
|
||||
@@ -355,25 +354,6 @@ class FrigateApp:
|
||||
)
|
||||
self.dispatcher.profile_manager = self.profile_manager
|
||||
|
||||
def restore_active_profile(self) -> None:
|
||||
"""Re-activate the persisted profile after subscribers are connected.
|
||||
|
||||
ZMQ PUB/SUB drops messages with no subscribers, so activation must
|
||||
run after every config_updater subscriber is up.
|
||||
"""
|
||||
if self.profile_manager is None:
|
||||
return
|
||||
|
||||
persisted = ProfileManager.load_persisted_profile()
|
||||
if persisted and any(
|
||||
persisted in cam.profiles for cam in self.config.cameras.values()
|
||||
):
|
||||
logger.info("Restoring persisted profile '%s'", persisted)
|
||||
# runtime overrides are layered on top via restore_runtime_state()
|
||||
self.profile_manager.activate_profile(
|
||||
persisted, clear_runtime_overrides=False
|
||||
)
|
||||
|
||||
def start_detectors(self) -> None:
|
||||
for name in self.config.cameras.keys():
|
||||
try:
|
||||
@@ -622,6 +602,13 @@ class FrigateApp:
|
||||
self.start_detectors()
|
||||
self.init_dispatcher()
|
||||
self.init_profile_manager()
|
||||
|
||||
# workers get a copy of the config and can miss the broadcast below, so
|
||||
# apply both layers here. must stay after init_profile_manager(), which
|
||||
# snapshots the base config that profile deactivation resets to
|
||||
self.profile_manager.restore_persisted_profile_to_config()
|
||||
self.dispatcher.reapply_runtime_state_to_config()
|
||||
|
||||
self.init_embeddings_client()
|
||||
self.start_video_output_processor()
|
||||
self.start_ptz_autotracker()
|
||||
@@ -636,8 +623,9 @@ class FrigateApp:
|
||||
self.start_record_cleanup()
|
||||
self.start_watchdog()
|
||||
|
||||
# restore persisted runtime overrides on top of config
|
||||
self.restore_active_profile()
|
||||
# publish for the recording/review/embeddings processes, which start
|
||||
# before the config can be corrected, and for the retained MQTT states
|
||||
self.profile_manager.restore_persisted_profile()
|
||||
self.dispatcher.restore_runtime_state()
|
||||
|
||||
self.init_auth()
|
||||
|
||||
@@ -77,6 +77,11 @@ class MqttClient(Communicator):
|
||||
"ON" if camera.audio.enabled_in_config else "OFF",
|
||||
retain=True,
|
||||
)
|
||||
self.publish(
|
||||
f"{camera_name}/audio_transcription/state",
|
||||
"ON" if camera.audio_transcription.live_enabled else "OFF",
|
||||
retain=True,
|
||||
)
|
||||
self.publish(
|
||||
f"{camera_name}/detect/state",
|
||||
"ON" if camera.detect.enabled else "OFF",
|
||||
@@ -258,6 +263,7 @@ class MqttClient(Communicator):
|
||||
"snapshots",
|
||||
"detect",
|
||||
"audio",
|
||||
"audio_transcription",
|
||||
"motion",
|
||||
"improve_contrast",
|
||||
"ptz_autotracker",
|
||||
|
||||
@@ -1,10 +1,18 @@
|
||||
from pydantic import Field
|
||||
from pydantic import Field, model_validator
|
||||
|
||||
from .base import FrigateBaseModel
|
||||
|
||||
__all__ = ["IPv6Config", "ListenConfig", "NetworkingConfig"]
|
||||
|
||||
|
||||
def parse_listen_port(value: int | str) -> int:
|
||||
"""Return the port number from a bare port or an "address:port" value."""
|
||||
if isinstance(value, str):
|
||||
return int(value.split(":")[-1])
|
||||
|
||||
return value
|
||||
|
||||
|
||||
class IPv6Config(FrigateBaseModel):
|
||||
enabled: bool = Field(
|
||||
default=False,
|
||||
@@ -25,6 +33,21 @@ class ListenConfig(FrigateBaseModel):
|
||||
description="External listening port for Frigate (default 8971).",
|
||||
)
|
||||
|
||||
@property
|
||||
def internal_port(self) -> int:
|
||||
return parse_listen_port(self.internal)
|
||||
|
||||
@property
|
||||
def external_port(self) -> int:
|
||||
return parse_listen_port(self.external)
|
||||
|
||||
@model_validator(mode="after")
|
||||
def validate_distinct_ports(self) -> "ListenConfig":
|
||||
if self.internal_port == self.external_port:
|
||||
raise ValueError("internal and external must listen on different ports")
|
||||
|
||||
return self
|
||||
|
||||
|
||||
class NetworkingConfig(FrigateBaseModel):
|
||||
ipv6: IPv6Config = Field(
|
||||
|
||||
@@ -169,6 +169,93 @@ class ProfileManager:
|
||||
self.config.active_profile = None
|
||||
self._persist_active_profile(None)
|
||||
|
||||
def _validate_profile_name(self, profile_name: str | None) -> str | None:
|
||||
"""Return an error message if the name is not a defined profile."""
|
||||
if profile_name is not None and profile_name not in self.config.profiles:
|
||||
return f"Profile '{profile_name}' is not defined in the profiles section"
|
||||
|
||||
return None
|
||||
|
||||
def _apply_to_config(
|
||||
self, profile_name: str | None
|
||||
) -> tuple[dict[str, set[str]], str | None]:
|
||||
"""Reset every camera to base, then apply the named profile on top.
|
||||
|
||||
Returns the changed camera/section pairs, plus an error message if
|
||||
applying the profile failed partway through.
|
||||
"""
|
||||
changed: dict[str, set[str]] = {}
|
||||
|
||||
self._reset_to_base(changed)
|
||||
|
||||
if profile_name is not None:
|
||||
err = self._apply_profile_overrides(profile_name, changed)
|
||||
if err:
|
||||
return changed, err
|
||||
|
||||
return changed, None
|
||||
|
||||
def apply_profile_to_config(self, profile_name: str | None) -> str | None:
|
||||
"""Apply a profile to the in-memory config, without publishing it.
|
||||
|
||||
Safe to call ahead of activate_profile: both reset to the base config
|
||||
first, so the later call re-derives the same state and still reports
|
||||
every section as changed.
|
||||
|
||||
Returns:
|
||||
None on success, or an error message string on failure.
|
||||
"""
|
||||
err = self._validate_profile_name(profile_name)
|
||||
|
||||
if err:
|
||||
return err
|
||||
|
||||
return self._apply_to_config(profile_name)[1]
|
||||
|
||||
def _persisted_profile_to_restore(self) -> str | None:
|
||||
"""Return the persisted profile name, if it still applies to a camera."""
|
||||
persisted = self.load_persisted_profile()
|
||||
|
||||
if not persisted or not any(
|
||||
persisted in cam.profiles for cam in self.config.cameras.values()
|
||||
):
|
||||
return None
|
||||
|
||||
return persisted
|
||||
|
||||
def restore_persisted_profile_to_config(self) -> None:
|
||||
"""Restore the persisted profile into the config, without publishing.
|
||||
|
||||
Called before worker processes start, so they are handed a config that
|
||||
already carries the profile rather than relying on the broadcast that
|
||||
restore_persisted_profile() sends later.
|
||||
"""
|
||||
persisted = self._persisted_profile_to_restore()
|
||||
|
||||
if persisted is None:
|
||||
return
|
||||
|
||||
err = self.apply_profile_to_config(persisted)
|
||||
|
||||
if err:
|
||||
logger.error("Failed to apply persisted profile '%s': %s", persisted, err)
|
||||
|
||||
def restore_persisted_profile(self) -> None:
|
||||
"""Re-activate the persisted profile once subscribers are connected.
|
||||
|
||||
The config already carries the profile; this pass publishes it for the
|
||||
processes that start before the config can be corrected, and for the
|
||||
retained MQTT states.
|
||||
"""
|
||||
persisted = self._persisted_profile_to_restore()
|
||||
|
||||
if persisted is None:
|
||||
return
|
||||
|
||||
logger.info("Restoring persisted profile '%s'", persisted)
|
||||
# runtime overrides are layered on top by the dispatcher's replay
|
||||
self.activate_profile(persisted, clear_runtime_overrides=False)
|
||||
|
||||
def activate_profile(
|
||||
self,
|
||||
profile_name: str | None,
|
||||
@@ -187,23 +274,16 @@ class ProfileManager:
|
||||
Returns:
|
||||
None on success, or an error message string on failure.
|
||||
"""
|
||||
if profile_name is not None:
|
||||
if profile_name not in self.config.profiles:
|
||||
return (
|
||||
f"Profile '{profile_name}' is not defined in the profiles section"
|
||||
)
|
||||
err = self._validate_profile_name(profile_name)
|
||||
|
||||
if err:
|
||||
return err
|
||||
|
||||
# Track which camera/section pairs get changed for ZMQ publishing
|
||||
changed: dict[str, set[str]] = {}
|
||||
changed, err = self._apply_to_config(profile_name)
|
||||
|
||||
# Reset all cameras to base config
|
||||
self._reset_to_base(changed)
|
||||
|
||||
# Apply new profile overrides if activating
|
||||
if profile_name is not None:
|
||||
err = self._apply_profile_overrides(profile_name, changed)
|
||||
if err:
|
||||
return err
|
||||
if err:
|
||||
return err
|
||||
|
||||
# Publish ZMQ updates only for sections that actually changed
|
||||
self._publish_updates(changed)
|
||||
|
||||
@@ -0,0 +1,174 @@
|
||||
"""Tests that the internal port trusted by /auth cannot be moved at runtime."""
|
||||
|
||||
import os
|
||||
import tempfile
|
||||
import unittest
|
||||
from unittest.mock import MagicMock, Mock, patch
|
||||
|
||||
import ruamel.yaml
|
||||
from fastapi import Request
|
||||
|
||||
from frigate.api.auth import get_allowed_cameras_for_filter, get_current_user
|
||||
from frigate.api.fastapi_app import create_fastapi_app
|
||||
from frigate.config import FrigateConfig
|
||||
from frigate.config.camera.updater import CameraConfigUpdatePublisher
|
||||
from frigate.const import JWT_SECRET_ENV_VAR
|
||||
from frigate.models import Event, Recordings, ReviewSegment
|
||||
from frigate.test.http_api.base_http_test import AuthTestClient, BaseTestHttp
|
||||
|
||||
|
||||
@patch.dict(os.environ, {JWT_SECRET_ENV_VAR: "test-secret"})
|
||||
class TestAuthInternalPort(BaseTestHttp):
|
||||
"""/auth grants anonymous admin by port, so that port must stay put.
|
||||
|
||||
nginx binds its listeners once at container start and never reloads them,
|
||||
but /api/config/set can swap the live config object mid-process. If /auth
|
||||
read the port off the live config, saving networking.listen.internal would
|
||||
hand unauthenticated admin to whoever can reach the external port.
|
||||
"""
|
||||
|
||||
def setUp(self):
|
||||
super().setUp(models=[Event, Recordings, ReviewSegment])
|
||||
self.minimal_config = {
|
||||
"mqtt": {"host": "mqtt"},
|
||||
"auth": {"enabled": True},
|
||||
"networking": {"listen": {"internal": 5000, "external": 8971}},
|
||||
"cameras": {
|
||||
"front_door": {
|
||||
"ffmpeg": {
|
||||
"inputs": [
|
||||
{"path": "rtsp://10.0.0.1:554/video", "roles": ["detect"]}
|
||||
]
|
||||
},
|
||||
"detect": {
|
||||
"height": 1080,
|
||||
"width": 1920,
|
||||
"fps": 5,
|
||||
},
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
def _create_app(self):
|
||||
mock_publisher = Mock(spec=CameraConfigUpdatePublisher)
|
||||
mock_publisher.publisher = MagicMock()
|
||||
|
||||
app = create_fastapi_app(
|
||||
FrigateConfig(**self.minimal_config),
|
||||
self.db,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
mock_publisher,
|
||||
None,
|
||||
enforce_default_admin=False,
|
||||
)
|
||||
|
||||
async def mock_get_current_user(request: Request):
|
||||
return {
|
||||
"username": request.headers.get("remote-user"),
|
||||
"role": request.headers.get("remote-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 _write_config_file(self):
|
||||
"""Write the minimal config to a temp YAML file and return the path."""
|
||||
yaml = ruamel.yaml.YAML()
|
||||
f = tempfile.NamedTemporaryFile(mode="w", suffix=".yml", delete=False)
|
||||
yaml.dump(self.minimal_config, f)
|
||||
f.close()
|
||||
return f.name
|
||||
|
||||
def test_internal_port_is_anonymous_admin(self):
|
||||
app = self._create_app()
|
||||
|
||||
with AuthTestClient(app) as client:
|
||||
resp = client.get("/auth", headers={"x-server-port": "5000"})
|
||||
|
||||
self.assertEqual(resp.status_code, 202)
|
||||
self.assertEqual(resp.headers["remote-user"], "anonymous")
|
||||
self.assertEqual(resp.headers["remote-role"], "admin")
|
||||
|
||||
def test_external_port_requires_auth(self):
|
||||
app = self._create_app()
|
||||
|
||||
with AuthTestClient(app) as client:
|
||||
resp = client.get("/auth", headers={"x-server-port": "8971"})
|
||||
|
||||
self.assertEqual(resp.status_code, 401)
|
||||
|
||||
def test_swapped_config_does_not_move_the_trusted_port(self):
|
||||
"""The live config is not what /auth trusts.
|
||||
|
||||
Stands in for every path that can rebind app.frigate_config while the
|
||||
process runs, whatever restart flag the caller claimed.
|
||||
"""
|
||||
app = self._create_app()
|
||||
|
||||
swapped = FrigateConfig(
|
||||
**{
|
||||
**self.minimal_config,
|
||||
"networking": {"listen": {"internal": 8971, "external": 5000}},
|
||||
}
|
||||
)
|
||||
app.frigate_config = swapped
|
||||
|
||||
with AuthTestClient(app) as client:
|
||||
resp = client.get("/auth", headers={"x-server-port": "8971"})
|
||||
self.assertEqual(resp.status_code, 401)
|
||||
|
||||
# nginx is still listening where it was told to at boot
|
||||
resp = client.get("/auth", headers={"x-server-port": "5000"})
|
||||
self.assertEqual(resp.status_code, 202)
|
||||
self.assertEqual(resp.headers["remote-role"], "admin")
|
||||
|
||||
@patch("frigate.api.app.find_config_file")
|
||||
def test_config_set_rejects_internal_matching_external(self, mock_find_config):
|
||||
"""Saving the internal port onto the external one is refused outright."""
|
||||
config_path = self._write_config_file()
|
||||
mock_find_config.return_value = config_path
|
||||
|
||||
try:
|
||||
app = self._create_app()
|
||||
|
||||
with AuthTestClient(app) as client:
|
||||
resp = client.put(
|
||||
"/config/set",
|
||||
json={
|
||||
"config_data": {"networking": {"listen": {"internal": 8971}}},
|
||||
"update_topic": "config/networking",
|
||||
"requires_restart": 1,
|
||||
},
|
||||
)
|
||||
|
||||
self.assertEqual(resp.status_code, 400)
|
||||
self.assertFalse(resp.json()["success"])
|
||||
|
||||
# the rejected save must not have reached the live config
|
||||
self.assertEqual(
|
||||
app.frigate_config.networking.listen.internal_port, 5000
|
||||
)
|
||||
|
||||
resp = client.get("/auth", headers={"x-server-port": "8971"})
|
||||
self.assertEqual(resp.status_code, 401)
|
||||
|
||||
with open(config_path) as f:
|
||||
self.assertNotIn("8971", f.read().split("external")[0])
|
||||
finally:
|
||||
os.unlink(config_path)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main(verbosity=2)
|
||||
@@ -5,6 +5,7 @@ import tempfile
|
||||
import unittest
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
from frigate.app import FrigateApp
|
||||
from frigate.comms.dispatcher import Dispatcher
|
||||
from frigate.comms.runtime_state import RuntimeStatePersistence
|
||||
|
||||
@@ -363,5 +364,94 @@ class TestReapplyRuntimeStateToConfig(unittest.TestCase):
|
||||
dispatcher.reapply_runtime_state_to_config()
|
||||
|
||||
|
||||
class TestStartupAppliesConfigLayersBeforeWorkersStart(unittest.TestCase):
|
||||
"""Both layers must reach the config before config-carrying workers start.
|
||||
|
||||
A worker started before a layer is applied keeps the yaml value for the
|
||||
rest of the session: the config_updater broadcast sent later is dropped
|
||||
for subscribers that have not connected yet, and nothing re-sends it.
|
||||
"""
|
||||
|
||||
CONFIG_LAYERS = (
|
||||
"profile_manager.restore_persisted_profile_to_config",
|
||||
"dispatcher.reapply_runtime_state_to_config",
|
||||
)
|
||||
|
||||
# started with a copy of the camera config
|
||||
CONFIG_CARRYING_WORKERS = (
|
||||
"start_video_output_processor",
|
||||
"start_ptz_autotracker",
|
||||
"start_detected_frames_processor",
|
||||
"start_camera_processor",
|
||||
"start_audio_processor",
|
||||
)
|
||||
|
||||
def _start_call_order(self) -> list[str]:
|
||||
"""Return the names FrigateApp.start() calls, in order."""
|
||||
app = MagicMock()
|
||||
|
||||
with (
|
||||
patch("frigate.app.set_file_limit"),
|
||||
patch("frigate.app.cleanup_replay_cameras"),
|
||||
patch("frigate.app.reap_stale_exports"),
|
||||
patch("frigate.app.create_fastapi_app"),
|
||||
patch("frigate.app.uvicorn"),
|
||||
):
|
||||
FrigateApp.start(app)
|
||||
|
||||
return [name for name, _, _ in app.mock_calls]
|
||||
|
||||
def test_applied_before_any_config_carrying_worker(self) -> None:
|
||||
order = self._start_call_order()
|
||||
|
||||
for layer in self.CONFIG_LAYERS:
|
||||
for worker in self.CONFIG_CARRYING_WORKERS:
|
||||
self.assertLess(order.index(layer), order.index(worker))
|
||||
|
||||
def test_applied_after_the_dispatcher_exists(self) -> None:
|
||||
order = self._start_call_order()
|
||||
|
||||
for layer in self.CONFIG_LAYERS:
|
||||
self.assertLess(order.index("init_dispatcher"), order.index(layer))
|
||||
|
||||
def test_applied_after_the_profile_base_is_snapshotted(self) -> None:
|
||||
# ProfileManager snapshots the config as the "no profile" base that
|
||||
# deactivation resets to, so neither layer may be in the config yet
|
||||
order = self._start_call_order()
|
||||
|
||||
for layer in self.CONFIG_LAYERS:
|
||||
self.assertLess(order.index("init_profile_manager"), order.index(layer))
|
||||
|
||||
def test_layers_applied_in_order(self) -> None:
|
||||
# a runtime toggle is the layer the user set last, so it goes on top
|
||||
order = self._start_call_order()
|
||||
|
||||
self.assertLess(
|
||||
order.index("profile_manager.restore_persisted_profile_to_config"),
|
||||
order.index("dispatcher.reapply_runtime_state_to_config"),
|
||||
)
|
||||
|
||||
def test_overrides_still_re_applied_after_the_profile_is_restored(self) -> None:
|
||||
# activation resets the sections it owns to the base first, so the
|
||||
# overrides have to land on top again
|
||||
order = self._start_call_order()
|
||||
|
||||
self.assertLess(
|
||||
order.index("profile_manager.restore_persisted_profile"),
|
||||
order.index("dispatcher.restore_runtime_state"),
|
||||
)
|
||||
|
||||
def test_broadcast_replay_still_runs_at_the_end(self) -> None:
|
||||
# the broadcast is the only channel for the recording, review, and
|
||||
# embeddings processes, which start before the config can be corrected
|
||||
order = self._start_call_order()
|
||||
|
||||
for replay in (
|
||||
"profile_manager.restore_persisted_profile",
|
||||
"dispatcher.restore_runtime_state",
|
||||
):
|
||||
self.assertLess(order.index("start_audio_processor"), order.index(replay))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
"""Tests for networking config validation."""
|
||||
|
||||
import unittest
|
||||
|
||||
from pydantic import ValidationError
|
||||
|
||||
from frigate.config.network import ListenConfig
|
||||
|
||||
|
||||
class TestListenConfig(unittest.TestCase):
|
||||
def test_defaults_are_distinct(self):
|
||||
listen = ListenConfig()
|
||||
|
||||
self.assertEqual(listen.internal_port, 5000)
|
||||
self.assertEqual(listen.external_port, 8971)
|
||||
|
||||
def test_address_and_port_string_is_parsed(self):
|
||||
listen = ListenConfig(internal="127.0.0.1:5000", external="0.0.0.0:8971")
|
||||
|
||||
self.assertEqual(listen.internal_port, 5000)
|
||||
self.assertEqual(listen.external_port, 8971)
|
||||
|
||||
def test_identical_ports_rejected(self):
|
||||
with self.assertRaises(ValidationError):
|
||||
ListenConfig(internal=8971, external=8971)
|
||||
|
||||
def test_same_port_on_different_addresses_rejected(self):
|
||||
# nginx would accept these as distinct listeners, but /auth decides on
|
||||
# the port alone, so the external one would inherit anonymous admin
|
||||
with self.assertRaises(ValidationError):
|
||||
ListenConfig(internal="127.0.0.1:8971", external="0.0.0.0:8971")
|
||||
|
||||
def test_distinct_ports_accepted(self):
|
||||
listen = ListenConfig(internal=5001, external="0.0.0.0:8971")
|
||||
|
||||
self.assertEqual(listen.internal_port, 5001)
|
||||
self.assertEqual(listen.external_port, 8971)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main(verbosity=2)
|
||||
@@ -785,6 +785,98 @@ class TestProfileManager(unittest.TestCase):
|
||||
manager.activate_profile("armed", clear_runtime_overrides=False)
|
||||
dispatcher.clear_runtime_state.assert_not_called()
|
||||
|
||||
def test_apply_profile_to_config_mutates_the_config(self):
|
||||
"""The config-only half applies the same overrides as activation."""
|
||||
err = self.manager.apply_profile_to_config("armed")
|
||||
assert err is None
|
||||
|
||||
front = self.config.cameras["front"]
|
||||
assert front.notifications.enabled is True
|
||||
assert front.objects.track == ["person", "car", "package"]
|
||||
|
||||
def test_apply_profile_to_config_makes_no_zmq_mqtt_or_disk_writes(self):
|
||||
"""Workers are started with the values, so nothing is published yet."""
|
||||
dispatcher = MagicMock()
|
||||
manager = ProfileManager(self.config, self.mock_updater, dispatcher)
|
||||
|
||||
with patch.object(ProfileManager, "_persist_active_profile") as mock_persist:
|
||||
manager.apply_profile_to_config("armed")
|
||||
|
||||
self.mock_updater.publish_update.assert_not_called()
|
||||
dispatcher.publish.assert_not_called()
|
||||
mock_persist.assert_not_called()
|
||||
# bookkeeping stays with activate_profile
|
||||
assert self.config.active_profile is None
|
||||
|
||||
def test_apply_profile_to_config_rejects_an_unknown_profile(self):
|
||||
err = self.manager.apply_profile_to_config("nonexistent")
|
||||
assert err is not None
|
||||
assert "not defined" in err
|
||||
|
||||
def test_restore_persisted_profile_to_config_applies_it(self):
|
||||
"""The startup config pass restores what was persisted."""
|
||||
with patch.object(
|
||||
ProfileManager, "load_persisted_profile", return_value="armed"
|
||||
):
|
||||
self.manager.restore_persisted_profile_to_config()
|
||||
|
||||
assert self.config.cameras["front"].notifications.enabled is True
|
||||
# still the config-only half, so nothing is published or persisted
|
||||
self.mock_updater.publish_update.assert_not_called()
|
||||
assert self.config.active_profile is None
|
||||
|
||||
def test_restore_persisted_profile_to_config_no_op_when_none_persisted(self):
|
||||
with patch.object(ProfileManager, "load_persisted_profile", return_value=None):
|
||||
self.manager.restore_persisted_profile_to_config()
|
||||
|
||||
assert self.config.cameras["front"].notifications.enabled is False
|
||||
|
||||
def test_restore_persisted_profile_to_config_ignores_a_stale_name(self):
|
||||
"""A profile no longer offered by any camera must not be applied."""
|
||||
with patch.object(
|
||||
ProfileManager, "load_persisted_profile", return_value="ghost"
|
||||
):
|
||||
self.manager.restore_persisted_profile_to_config()
|
||||
|
||||
assert self.config.cameras["front"].notifications.enabled is False
|
||||
|
||||
@patch.object(ProfileManager, "_persist_active_profile")
|
||||
def test_restore_persisted_profile_activates_and_publishes(self, mock_persist):
|
||||
"""The startup publish pass runs a full activation."""
|
||||
dispatcher = MagicMock()
|
||||
manager = ProfileManager(self.config, self.mock_updater, dispatcher)
|
||||
|
||||
with patch.object(
|
||||
ProfileManager, "load_persisted_profile", return_value="armed"
|
||||
):
|
||||
manager.restore_persisted_profile()
|
||||
|
||||
assert self.config.active_profile == "armed"
|
||||
self.mock_updater.publish_update.assert_called()
|
||||
# a startup replay must not wipe the runtime overrides layered on top
|
||||
dispatcher.clear_runtime_state.assert_not_called()
|
||||
|
||||
@patch.object(ProfileManager, "_persist_active_profile")
|
||||
def test_activation_after_apply_still_publishes_every_section(self, mock_persist):
|
||||
"""Re-deriving the same state must not skip the broadcast.
|
||||
|
||||
The processes that started before the config was corrected have no
|
||||
other channel.
|
||||
"""
|
||||
self.manager.apply_profile_to_config("armed")
|
||||
self.mock_updater.publish_update.reset_mock()
|
||||
|
||||
err = self.manager.activate_profile("armed", clear_runtime_overrides=False)
|
||||
assert err is None
|
||||
|
||||
published = {
|
||||
call.args[0].update_type.name
|
||||
for call in self.mock_updater.publish_update.call_args_list
|
||||
}
|
||||
assert "notifications" in published
|
||||
assert "objects" in published
|
||||
assert self.config.active_profile == "armed"
|
||||
|
||||
@patch.object(ProfileManager, "_persist_active_profile")
|
||||
def test_update_config_preserves_runtime_state_with_active_profile(
|
||||
self, mock_persist
|
||||
|
||||
@@ -35,6 +35,11 @@ logger = logging.getLogger(__name__)
|
||||
GRID_SIZE = 8
|
||||
|
||||
|
||||
def create_empty_regions_grid() -> list[list[dict[str, Any]]]:
|
||||
"""Create a region grid with no learned sizes."""
|
||||
return [[{"sizes": []} for _ in range(GRID_SIZE)] for _ in range(GRID_SIZE)]
|
||||
|
||||
|
||||
def get_camera_regions_grid(
|
||||
name: str,
|
||||
detect: DetectConfig,
|
||||
@@ -47,12 +52,7 @@ def get_camera_regions_grid(
|
||||
grid = regions.grid
|
||||
last_update = regions.last_update
|
||||
except DoesNotExist:
|
||||
grid = []
|
||||
for x in range(GRID_SIZE):
|
||||
row = []
|
||||
for y in range(GRID_SIZE):
|
||||
row.append({"sizes": []})
|
||||
grid.append(row)
|
||||
grid = create_empty_regions_grid()
|
||||
last_update = 0
|
||||
|
||||
# get events for timeline entries
|
||||
|
||||
@@ -867,8 +867,8 @@
|
||||
"description": "Ordre numèric utilitzat per ordenar la càmera a la interfície d'usuari (taulell de control i llistes per defecte); els nombres més grans apareixen més tard."
|
||||
},
|
||||
"dashboard": {
|
||||
"label": "Mostra a l'interfície d'usuari",
|
||||
"description": "Estableix si aquesta càmera és visible a tot arreu a la interfície d'usuari de la Frigate. Desactivar això requerirà editar manualment la configuració per tornar a veure aquesta càmera a la interfície d'usuari."
|
||||
"label": "Mostra al tauler en directe",
|
||||
"description": "Alterna si aquesta càmera és visible al tauler de control en directe de totes les càmeres per defecte. La càmera roman disponible a tot arreu a la interfície d'usuari, inclosos els grups i la configuració de la càmera."
|
||||
},
|
||||
"review": {
|
||||
"label": "Mostra en la revisió",
|
||||
|
||||
@@ -2338,8 +2338,8 @@
|
||||
"description": "Ordre numèric utilitzat per ordenar la càmera a la interfície d'usuari (taulell de control i llistes per defecte); els nombres més grans apareixen més tard."
|
||||
},
|
||||
"dashboard": {
|
||||
"label": "Mostra a la interfície",
|
||||
"description": "Estableix si aquesta càmera és visible a tot arreu a la interfície d'usuari de Frigate. Desactivar això requerirà editar manualment la configuració per tornar a veure aquesta càmera a la interfície d'usuari."
|
||||
"label": "Mostra al tauler en directe",
|
||||
"description": "Alterna si aquesta càmera és visible al tauler de control en directe de totes les càmeres per defecte. La càmera roman disponible a tot arreu a la interfície d'usuari, inclosos els grups i la configuració de la càmera."
|
||||
},
|
||||
"review": {
|
||||
"label": "Mostra en la revisió",
|
||||
|
||||
@@ -126,5 +126,7 @@
|
||||
"baby_stroller": "Cotxet",
|
||||
"rickshaw": "Ricksaw",
|
||||
"Rodent": "Rosegador",
|
||||
"rodent": "Rosegador"
|
||||
"rodent": "Rosegador",
|
||||
"possum": "Possum",
|
||||
"garbage_truck": "Camió de brossa"
|
||||
}
|
||||
|
||||
@@ -76,7 +76,7 @@
|
||||
"menuItem": "Visualitza les vistes prèvies del moviment",
|
||||
"title": "Vista prèvia del moviment: {{camera}}",
|
||||
"mobileSettingsTitle": "Configuració de la vista prèvia del moviment",
|
||||
"mobileSettingsDesc": "Ajusteu la velocitat de reproducció i l'enfosquiment, i trieu una data per a revisar clips només en moviment.",
|
||||
"mobileSettingsDesc": "Ajusteu la velocitat de reproducció, l'enfosquiment i l'escapçament, i trieu una data per a revisar clips només en moviment.",
|
||||
"dim": "Atenuar",
|
||||
"dimAria": "Ajusta la intensitat de l'enfosquiment",
|
||||
"dimDesc": "Incrementa l'enfosquiment per augmentar la visibilitat de l'àrea de moviment.",
|
||||
@@ -89,6 +89,9 @@
|
||||
"seekAria": "Cerca el reproductor {{camera}} a {{time}}",
|
||||
"filter": "Filtre",
|
||||
"filterDesc": "Seleccioneu àrees per a mostrar només clips amb moviment en aquestes regions.",
|
||||
"filterClear": "Neteja"
|
||||
"filterClear": "Neteja",
|
||||
"crop": "Escapça per filtrar",
|
||||
"cropAria": "Commuta la vista prèvia d'escapçament a les àrees filtrades",
|
||||
"cropDesc": "Amplia les vistes prèvies a les àrees de filtre seleccionades en lloc de mostrar el fotograma complet."
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1330,7 +1330,7 @@
|
||||
"webuiUrlHelp": "URL per a visitar la interfície d'usuari web de la càmera directament des de la vista de depuració. Deixeu-ho en blanc per desactivar l'enllaç.",
|
||||
"webuiUrlInvalid": "Ha de ser un URL vàlid (p. ex., https://example.com).",
|
||||
"dashboardLabel": "Mostra al tauler en directe",
|
||||
"dashboardHelp": "Mostra aquesta càmera al Tauler en viu.",
|
||||
"dashboardHelp": "Mostra aquesta càmera al tauler de control en directe predeterminat de totes les càmeres. Es manté disponible a tot arreu, inclosos els grups de càmeres.",
|
||||
"reviewLabel": "Mostra a la ressenya",
|
||||
"reviewHelp": "Mostra aquesta càmera a Revisió, incloent el filtre de càmera, la revisió de moviment i la vista de l'historial."
|
||||
},
|
||||
@@ -1377,7 +1377,7 @@
|
||||
"deleteCameraDialog": {
|
||||
"title": "Suprimeix la càmera",
|
||||
"description": "Suprimir una càmera eliminarà permanentment tots els enregistraments, els objectes rastrejats i la configuració d'aquesta càmera. Qualsevol flux go2rtc associat amb aquesta càmera encara pot haver de ser eliminat manualment.",
|
||||
"selectPlaceholder": "Trieu la càmera...",
|
||||
"selectPlaceholder": "Trieu la càmera…",
|
||||
"confirmTitle": "N'estàs segur?",
|
||||
"confirmWarning": "Suprimir <strong>{{cameraName}}</strong> no es pot desfer.",
|
||||
"deleteExports": "Elimina també les exportacions d'aquesta càmera",
|
||||
|
||||
@@ -443,5 +443,60 @@
|
||||
"slap": "Plácnout",
|
||||
"smash": "Rozmlátit",
|
||||
"bouncing": "Odrážející",
|
||||
"scratch": "Škrábat"
|
||||
"scratch": "Škrábat",
|
||||
"spray": "Sprej",
|
||||
"pulse": "Pulz",
|
||||
"inside": "Uvnitř",
|
||||
"outside": "Venku",
|
||||
"reverberation": "Dozvuk",
|
||||
"echo": "Ozvěna",
|
||||
"noise": "Hluk",
|
||||
"mains_hum": "Síťový brum",
|
||||
"distortion": "Zkreslení",
|
||||
"sidetone": "Příposlech",
|
||||
"cacophony": "Kakofonie",
|
||||
"throbbing": "Pulzování",
|
||||
"vibration": "Vibrace",
|
||||
"sodeling": "Jódlování",
|
||||
"shofar": "Šofar",
|
||||
"slosh": "Šplouchání",
|
||||
"gush": "Příval",
|
||||
"whoosh": "Svištění",
|
||||
"thump": "Tlumená rána",
|
||||
"thunk": "Dutá rána",
|
||||
"effects_unit": "Efektová jednotka",
|
||||
"chorus_effect": "Chorus",
|
||||
"whack": "Úder",
|
||||
"breaking": "Rozbíjení",
|
||||
"whip": "Švihnutí",
|
||||
"flap": "Třepotání",
|
||||
"scrape": "Drhnutí",
|
||||
"rub": "Tření",
|
||||
"roll": "Kutálení",
|
||||
"crushing": "Drcení",
|
||||
"crumpling": "Mačkání",
|
||||
"tearing": "Trhání",
|
||||
"beep": "Pípnutí",
|
||||
"ping": "Ping",
|
||||
"ding": "Cinknutí",
|
||||
"clang": "Řinčení",
|
||||
"squeal": "Skřípění",
|
||||
"creak": "Vrzání",
|
||||
"rustle": "Šustění",
|
||||
"whir": "Hučení",
|
||||
"clatter": "Rachocení",
|
||||
"sizzle": "Prskání",
|
||||
"clicking": "Klikání",
|
||||
"clickety_clack": "Klapot",
|
||||
"rumble": "Dunění",
|
||||
"plop": "Žbluňknutí",
|
||||
"hum": "Brum",
|
||||
"zing": "Zvonivý tón",
|
||||
"boing": "Pružinový zvuk",
|
||||
"crunch": "Křupání",
|
||||
"sine_wave": "Sinusový tón",
|
||||
"harmonic": "Harmonický tón",
|
||||
"chirp_tone": "Klouzavý tón",
|
||||
"pump": "Pumpa",
|
||||
"basketball_bounce": "Basketbalový odraz"
|
||||
}
|
||||
|
||||
@@ -120,7 +120,19 @@
|
||||
"deleteNow": "Smazat hned",
|
||||
"next": "Další",
|
||||
"export": "Exportovat",
|
||||
"continue": "Pokračovat"
|
||||
"continue": "Pokračovat",
|
||||
"add": "Přidat",
|
||||
"applying": "Aplikuje se…",
|
||||
"undo": "Vrátit",
|
||||
"copiedToClipboard": "Zkopírováno do schránky",
|
||||
"modified": "Upraveno",
|
||||
"overridden": "Přepsáno",
|
||||
"resetToGlobal": "Obnovit globální nastavení",
|
||||
"resetToDefault": "Obnovit výchozí nastavení",
|
||||
"saveAll": "Uložit vše",
|
||||
"savingAll": "Ukládání…",
|
||||
"undoAll": "Vrátit vše",
|
||||
"retry": "Zkusit znovu"
|
||||
},
|
||||
"label": {
|
||||
"back": "Jdi zpět",
|
||||
@@ -213,7 +225,9 @@
|
||||
"gl": "Galego (Galicijština)",
|
||||
"id": "Bahasa Indonesia (Indonéština)",
|
||||
"ur": "اردو (Urdština)",
|
||||
"hr": "Hrvatski (Chorvatština)"
|
||||
"hr": "Hrvatski (Chorvatština)",
|
||||
"zhHant": "繁體中文 (Tradiční čínština)",
|
||||
"bs": "Bosanski (Bosenština)"
|
||||
},
|
||||
"theme": {
|
||||
"highcontrast": "Vysoký kontrast",
|
||||
@@ -251,7 +265,11 @@
|
||||
"faceLibrary": "Knihovna Obličejů",
|
||||
"configurationEditor": "Editor Konfigurace",
|
||||
"withSystem": "Systém",
|
||||
"classification": "Klasifikace"
|
||||
"classification": "Klasifikace",
|
||||
"profiles": "Profily",
|
||||
"actions": "Akce",
|
||||
"features": "Funkce",
|
||||
"chat": "Chat"
|
||||
},
|
||||
"pagination": {
|
||||
"previous": {
|
||||
@@ -282,7 +300,8 @@
|
||||
"error": {
|
||||
"title": "Chyba při ukládání změn konfigurace: {{errorMessage}}",
|
||||
"noMessage": "Chyba při ukládání změn konfigurace"
|
||||
}
|
||||
},
|
||||
"success": "Změny konfigurace byly úspěšně uloženy."
|
||||
}
|
||||
},
|
||||
"role": {
|
||||
@@ -303,5 +322,10 @@
|
||||
},
|
||||
"information": {
|
||||
"pixels": "{{area}}px"
|
||||
},
|
||||
"no_items": "Žádné položky",
|
||||
"validation_errors": "Chyby ověření",
|
||||
"credentialField": {
|
||||
"savedPlaceholder": "Uloženo – ponechte prázdné pro zachování aktuální hodnoty"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -68,7 +68,10 @@
|
||||
"desc": "Vyberte kamery pro tuto skupinu."
|
||||
},
|
||||
"icon": "Ikona",
|
||||
"success": "Skupina kamer {{name}} byla uložena."
|
||||
"success": "Skupina kamer {{name}} byla uložena.",
|
||||
"showAll": "Zobrazit všechny skupiny kamer",
|
||||
"showLess": "Zobrazit méně",
|
||||
"editGroups": "Upravit skupiny kamer"
|
||||
},
|
||||
"debug": {
|
||||
"options": {
|
||||
@@ -82,6 +85,7 @@
|
||||
"regions": "Kraje",
|
||||
"timestamp": "Časové razítko",
|
||||
"boundingBox": "Ohraničení",
|
||||
"mask": "Maska"
|
||||
"mask": "Maska",
|
||||
"paths": "Cesty"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,7 +6,8 @@
|
||||
"title": "Frigate restartuje",
|
||||
"content": "Tato stránka bude obnovena za {{countdown}} sekund.",
|
||||
"button": "Vynutit opětovné načtení"
|
||||
}
|
||||
},
|
||||
"description": "Frigate bude během restartu krátce nedostupný."
|
||||
},
|
||||
"explore": {
|
||||
"plus": {
|
||||
@@ -19,6 +20,9 @@
|
||||
},
|
||||
"state": {
|
||||
"submitted": "Odesláno"
|
||||
},
|
||||
"toast": {
|
||||
"error": "Odeslání do Frigate+ se nezdařilo. Zkontrolujte své síťové připojení a zkuste to znovu."
|
||||
}
|
||||
},
|
||||
"submitToPlus": {
|
||||
@@ -74,7 +78,11 @@
|
||||
"endTimeMustAfterStartTime": "Čas konce musí být po čase začátku",
|
||||
"noVaildTimeSelected": "Není vybráno žádné platné časové období"
|
||||
},
|
||||
"view": "Zobrazení"
|
||||
"view": "Zobrazení",
|
||||
"queued": "Export zařazen do fronty. Průběh zobrazíte na stránce Exporty.",
|
||||
"batchSuccess_one": "Začal 1 export. Nyní se otevírá případ.",
|
||||
"batchSuccess_few": "Začaly {{count}} exporty. Nyní se otevírá případ.",
|
||||
"batchSuccess_other": "Začalo {{count}} exportů. Nyní se otevírá případ."
|
||||
},
|
||||
"fromTimeline": {
|
||||
"saveExport": "Uložit export",
|
||||
@@ -82,6 +90,65 @@
|
||||
},
|
||||
"name": {
|
||||
"placeholder": "Jméno exportu"
|
||||
},
|
||||
"case": {
|
||||
"newCaseOption": "Vytvořit nový případ",
|
||||
"newCaseNamePlaceholder": "Název nového případu",
|
||||
"newCaseDescriptionPlaceholder": "Popis případu",
|
||||
"label": "Případ",
|
||||
"nonAdminHelp": "Pro tyto exporty bude vytvořen nový případ.",
|
||||
"placeholder": "Vyberte případ"
|
||||
},
|
||||
"queueing": "Přidávání exportu do fronty…",
|
||||
"tabs": {
|
||||
"export": "Jedna kamera",
|
||||
"multiCamera": "Vícekamerový"
|
||||
},
|
||||
"multiCamera": {
|
||||
"timeRange": "Časový rozsah",
|
||||
"selectFromTimeline": "Vybrat z časové osy",
|
||||
"cameraSelection": "Kamery",
|
||||
"cameraSelectionHelp": "Kamery se sledovanými objekty v tomto časovém rozsahu jsou předem vybrány",
|
||||
"searchOrSelectGroup": "Vyhledejte nebo vyberte skupinu kamer…",
|
||||
"selectAll": "Vybrat všechny kamery",
|
||||
"clearSelection": "Zrušit výběr",
|
||||
"selectWithActivity": "Kamery se sledovanými objekty",
|
||||
"selectGroup": "Vybrat skupinu",
|
||||
"noMatchingCameras": "Žádná kamera neodpovídá vašemu hledání",
|
||||
"selectedCount": "{{selected}} / {{total}} vybráno",
|
||||
"checkingActivity": "Kontrola aktivity kamer…",
|
||||
"noCameras": "Nejsou k dispozici žádné kamery",
|
||||
"detectionCount_one": "1 sledovaný objekt",
|
||||
"detectionCount_few": "{{count}} sledované objekty",
|
||||
"detectionCount_other": "{{count}} sledovaných objektů",
|
||||
"nameLabel": "Název exportu",
|
||||
"namePlaceholder": "Volitelný základní název pro tyto exporty",
|
||||
"queueingButton": "Příprava exportů…",
|
||||
"exportButton_one": "Exportovat 1 kameru",
|
||||
"exportButton_few": "Exportovat {{count}} kamery",
|
||||
"exportButton_other": "Exportovat {{count}} kamer"
|
||||
},
|
||||
"multi": {
|
||||
"title_one": "Exportovat 1 kontrolu",
|
||||
"title_few": "Exportovat {{count}} kontroly",
|
||||
"title_other": "Exportovat {{count}} kontrol",
|
||||
"description": "Exportujte každou vybranou kontrolu. Všechny exporty budou seskupeny do jednoho případu.",
|
||||
"descriptionNoCase": "Exportujte každou vybranou kontrolu.",
|
||||
"caseNamePlaceholder": "Export kontroly – {{date}}",
|
||||
"exportButton_one": "Exportovat 1 kontrolu",
|
||||
"exportButton_few": "Exportovat {{count}} kontroly",
|
||||
"exportButton_other": "Exportovat {{count}} kontrol",
|
||||
"exportingButton": "Exportování…",
|
||||
"toast": {
|
||||
"started_one": "Spuštěn 1 export. Otevírá se případ.",
|
||||
"started_few": "Spuštěny {{count}} exporty. Otevírá se případ.",
|
||||
"started_other": "Spuštěno {{count}} exportů. Otevírá se případ.",
|
||||
"startedNoCase_one": "Spuštěn 1 export.",
|
||||
"startedNoCase_few": "Spuštěny {{count}} exporty.",
|
||||
"startedNoCase_other": "Spuštěno {{count}} exportů.",
|
||||
"partial": "Spuštěno {{successful}} z {{total}} exportů. Selhalo: {{failedItems}}",
|
||||
"failed": "Nepodařilo se spustit {{total}} exportů. Selhalo: {{failedItems}}"
|
||||
}
|
||||
}
|
||||
},
|
||||
"streaming": {
|
||||
|
||||
@@ -309,7 +309,8 @@
|
||||
"triggers": "Spouštěče",
|
||||
"cameraManagement": "Správa",
|
||||
"cameraReview": "Kontrola",
|
||||
"roles": "Role"
|
||||
"roles": "Role",
|
||||
"profiles": "Profily"
|
||||
},
|
||||
"dialog": {
|
||||
"unsavedChanges": {
|
||||
|
||||
@@ -206,5 +206,140 @@
|
||||
"whale_vocalization": "Hvallyde",
|
||||
"music": "Musik",
|
||||
"musical_instrument": "Musikinstrument",
|
||||
"plucked_string_instrument": "Strengeinstrument"
|
||||
"plucked_string_instrument": "Strengeinstrument",
|
||||
"zither": "Citar",
|
||||
"ukulele": "Ukulele",
|
||||
"piano": "Piano",
|
||||
"electric_piano": "Elektrisk Piano",
|
||||
"organ": "orgel",
|
||||
"electronic_organ": "Elektrisk orgel",
|
||||
"hammond_organ": "Hammond orgel",
|
||||
"synthesizer": "Synthesizer",
|
||||
"sampler": "Sampler",
|
||||
"harpsichord": "Mundharmonika",
|
||||
"percussion": "Percussion",
|
||||
"drum_kit": "Trommesæt",
|
||||
"drum_machine": "Trommemaskine",
|
||||
"drum": "Tromme",
|
||||
"timpani": "Pauker",
|
||||
"tabla": "Tabla",
|
||||
"cymbal": "Bækken",
|
||||
"hi_hat": "Hi-Hat",
|
||||
"wood_block": "Woodblock",
|
||||
"maraca": "Maraca",
|
||||
"gong": "Gong",
|
||||
"tubular_bells": "Rørklokker",
|
||||
"mallet_percussion": "Mallet percussion",
|
||||
"glockenspiel": "Klokkespil",
|
||||
"vibraphone": "Vibrafon",
|
||||
"steelpan": "olietønde",
|
||||
"orchestra": "Orkester",
|
||||
"brass_instrument": "Messingblæser",
|
||||
"french_horn": "Valdhorn",
|
||||
"bowed_string_instrument": "Stryger",
|
||||
"string_section": "Strygerdel",
|
||||
"pizzicato": "Pizzicato",
|
||||
"cello": "Cello",
|
||||
"double_bass": "Kontrabas",
|
||||
"wind_instrument": "Blæseinstrument",
|
||||
"church_bell": "Kirkeklokke",
|
||||
"jingle_bell": "Bjældeklang",
|
||||
"bicycle_bell": "Ringeklokke",
|
||||
"tuning_fork": "Stemmegaffel",
|
||||
"chime": "Kimen",
|
||||
"wind_chime": "Vindklokke",
|
||||
"accordion": "Harmonika",
|
||||
"theremin": "Theremin",
|
||||
"singing_bowl": "Syngeskål",
|
||||
"scratching": "Kradse",
|
||||
"pop_music": "Popmusik",
|
||||
"hip_hop_music": "Hip-Hopmusik",
|
||||
"beatboxing": "Beatboxing",
|
||||
"rock_music": "Rockmusik",
|
||||
"heavy_metal": "Heavymetal",
|
||||
"punk_rock": "Punkrock",
|
||||
"grunge": "Grunge",
|
||||
"progressive_rock": "Progressiv rock",
|
||||
"rock_and_roll": "Rock and Roll",
|
||||
"psychedelic_rock": "Psykedelisk rock",
|
||||
"rhythm_and_blues": "Rhythm and Blues",
|
||||
"soul_music": "Soulmusik",
|
||||
"reggae": "Reggae",
|
||||
"country": "Country",
|
||||
"disco": "Disko",
|
||||
"classical_music": "Klassisk musik",
|
||||
"electronic_music": "Elektronisk musik",
|
||||
"house_music": "Housemusik",
|
||||
"drum_and_bass": "Drum and Bass",
|
||||
"electronica": "Elektronisk musik",
|
||||
"electronic_dance_music": "Elektronisk dancemusik",
|
||||
"ambient_music": "Ambient musik",
|
||||
"trance_music": "Trancemusik",
|
||||
"music_of_latin_america": "Musik fra Latin Amerika",
|
||||
"salsa_music": "Salsamusik",
|
||||
"flamenco": "Flamenco",
|
||||
"music_for_children": "Musik for børn",
|
||||
"new-age_music": "New Age musik",
|
||||
"vocal_music": "Vokalmusik",
|
||||
"a_capella": "A Capella",
|
||||
"music_of_africa": "Musik fra Afrika",
|
||||
"afrobeat": "Afrobeat",
|
||||
"christian_music": "Kristen musik",
|
||||
"gospel_music": "Gospelmusik",
|
||||
"music_of_asia": "Musik fra Asian",
|
||||
"ska": "Ska",
|
||||
"traditional_music": "Traditionel musik",
|
||||
"independent_music": "Individual musik",
|
||||
"background_music": "Baggrundsmusik",
|
||||
"theme_music": "Temamusik",
|
||||
"jingle": "Jingle",
|
||||
"soundtrack_music": "Soundtrack musik",
|
||||
"video_game_music": "Videospil musik",
|
||||
"christmas_music": "Julemusik",
|
||||
"dance_music": "Dance musik",
|
||||
"wedding_music": "Bryllupsmusik",
|
||||
"happy_music": "Glad musik",
|
||||
"sad_music": "Trist musik",
|
||||
"tender_music": "Kærlighedsmusik",
|
||||
"exciting_music": "Spændende musik",
|
||||
"angry_music": "Vred musik",
|
||||
"scary_music": "Skræmmende musik",
|
||||
"rustling_leaves": "Raslende blade",
|
||||
"wind_noise": "Vindstøj",
|
||||
"rain_on_surface": "Regn på en overflade",
|
||||
"stream": "Strøm",
|
||||
"ocean": "Verdenshav",
|
||||
"steam": "Damp",
|
||||
"gurgling": "Gurgle",
|
||||
"crackle": "Knitren",
|
||||
"motor_vehicle": "Motoriseret køretøj",
|
||||
"toot": "Dytte",
|
||||
"car_alarm": "Bilalarm",
|
||||
"power_windows": "Elruder",
|
||||
"skidding": "Udskridning",
|
||||
"tire_squeal": "Dækhvin",
|
||||
"car_passing_by": "Forbikørende bil",
|
||||
"race_car": "Racerbil",
|
||||
"truck": "Lastbil",
|
||||
"air_brake": "Trykluftsbremse",
|
||||
"air_horn": "Trykluftshorn",
|
||||
"reversing_beeps": "Bakalarm",
|
||||
"ice_cream_truck": "Isbil",
|
||||
"emergency_vehicle": "Redningskøretøj",
|
||||
"police_car": "Politibil",
|
||||
"fire_engine": "Brandbil",
|
||||
"traffic_noise": "Trafikstøj",
|
||||
"rail_transport": "Godstogstransport",
|
||||
"train_whistle": "Togfløjt",
|
||||
"train_horn": "Toghorn",
|
||||
"railroad_car": "Togvogn",
|
||||
"train_wheels_squealing": "Skinneskrig",
|
||||
"subway": "Undergrund",
|
||||
"aircraft": "Fly",
|
||||
"aircraft_engine": "Flymotor",
|
||||
"jet_engine": "Jetmotor",
|
||||
"propeller": "Probel",
|
||||
"fixed-wing_aircraft": "Fastvingefly",
|
||||
"engine": "Motor",
|
||||
"light_engine": "Lille motor"
|
||||
}
|
||||
|
||||
@@ -30,6 +30,9 @@
|
||||
"ask_a": "Ist dieses Objekt ein <code>{{label}}</code>?",
|
||||
"ask_an": "Ist dieses Objekt ein <code>{{label}}</code>?",
|
||||
"ask_full": "Ist dieses Objekt ein <code>{{untranslatedLabel}}</code> ({{translatedLabel}})?"
|
||||
},
|
||||
"toast": {
|
||||
"error": "Übermittlung an Frigate+ fehlgeschlagen. Bitte überprüfen Sie Ihre Netzwerkverbindung und versuchen Sie es erneut."
|
||||
}
|
||||
},
|
||||
"submitToPlus": {
|
||||
@@ -65,7 +68,8 @@
|
||||
"error": {
|
||||
"endTimeMustAfterStartTime": "Die Endzeit darf nicht vor der Startzeit liegen",
|
||||
"failed": "Fehler beim Export in die Warteschlange: {{error}}",
|
||||
"noVaildTimeSelected": "Kein gültiger Zeitraum ausgewählt"
|
||||
"noVaildTimeSelected": "Kein gültiger Zeitraum ausgewählt",
|
||||
"noValidTimeSelected": "Kein gültiger Zeitraum ausgewählt"
|
||||
},
|
||||
"success": "Export erfolgreich gestartet. Die Datei befindet sich auf der Exportseite.",
|
||||
"view": "Ansicht",
|
||||
|
||||
@@ -545,6 +545,9 @@
|
||||
"max_concurrent": {
|
||||
"label": "Maximale Anzahl gleichzeitiger Exporte",
|
||||
"description": "Maximale Anzahl der gleichzeitig zu verarbeitenden Exportaufträge."
|
||||
},
|
||||
"chapters": {
|
||||
"label": "Kapitel-Metadaten zur Einbettung in exportierte Aufzeichnungen"
|
||||
}
|
||||
},
|
||||
"preview": {
|
||||
@@ -768,8 +771,8 @@
|
||||
"description": "Numerische Reihenfolge, nach der die Kamera in der Benutzeroberfläche sortiert wird (Standard-Dashboard und Listen); höhere Zahlen erscheinen später."
|
||||
},
|
||||
"dashboard": {
|
||||
"label": "In der Benutzeroberfläche anzeigen",
|
||||
"description": "Schalte ein, ob diese Kamera überall in der Benutzeroberfläche von „Frigate“ sichtbar ist. Wenn du diese Option deaktivierst, musst du die Konfiguration manuell bearbeiten, um diese Kamera wieder in der Benutzeroberfläche anzuzeigen."
|
||||
"label": "Im Live-Dashboard anzeigen",
|
||||
"description": "Legt fest, ob diese Kamera im Live-Dashboard ‚Alle Kameras‘ angezeigt wird. Die Kamera bleibt in allen anderen Bereichen der Benutzeroberfläche verfügbar, einschließlich Kamera-Gruppen und Einstellungen."
|
||||
},
|
||||
"review": {
|
||||
"label": "In der Überprüfung anzeigen",
|
||||
|
||||
@@ -1123,7 +1123,7 @@
|
||||
},
|
||||
"model_type": {
|
||||
"label": "Typ des Objekterkennungsmodells",
|
||||
"description": "Typ der Detektor-Modellarchitektur (ssd, yolox, yolonas), der von einigen Detektoren zur Optimierung verwendet wird."
|
||||
"description": "Typ der Detektor-Modellarchitektur (ssd, yolox, yolonas, yolo-generic, rfdetr, dfine), der von einigen Detektoren zur Optimierung verwendet wird."
|
||||
}
|
||||
},
|
||||
"model_path": {
|
||||
@@ -1400,6 +1400,9 @@
|
||||
"max_concurrent": {
|
||||
"label": "Maximale Anzahl gleichzeitiger Exporte",
|
||||
"description": "Maximale Anzahl der gleichzeitig zu verarbeitenden Exportaufträge."
|
||||
},
|
||||
"chapters": {
|
||||
"label": "Kapitel-Metadaten zur Einbettung in exportierte Aufzeichnungen"
|
||||
}
|
||||
},
|
||||
"preview": {
|
||||
@@ -1729,7 +1732,7 @@
|
||||
},
|
||||
"model_type": {
|
||||
"label": "Typ des Objekterkennungsmodells",
|
||||
"description": "Typ der Detektor-Modellarchitektur (ssd, yolox, yolonas), der von einigen Detektoren zur Optimierung verwendet wird."
|
||||
"description": "Typ der Detektor-Modellarchitektur (ssd, yolox, yolonas, yolo-generic, rfdetr, dfine), der von einigen Detektoren zur Optimierung verwendet wird."
|
||||
}
|
||||
},
|
||||
"genai": {
|
||||
@@ -1932,8 +1935,8 @@
|
||||
"description": "Numerische Reihenfolge, nach der die Kamera in der Benutzeroberfläche sortiert wird (Standard-Dashboard und Listen); höhere Zahlen erscheinen später."
|
||||
},
|
||||
"dashboard": {
|
||||
"label": "In der Benutzeroberfläche anzeigen",
|
||||
"description": "Schalte ein, ob diese Kamera überall in der Benutzeroberfläche von „Frigate“ sichtbar ist. Wenn du diese Option deaktivierst, musst du die Konfiguration manuell bearbeiten, um diese Kamera wieder in der Benutzeroberfläche anzuzeigen."
|
||||
"label": "Im Live-Dashboard anzeigen",
|
||||
"description": "Legt fest, ob diese Kamera im Live-Dashboard ‚Alle Kameras‘ angezeigt wird. Die Kamera bleibt in allen anderen Bereichen der Benutzeroberfläche verfügbar, einschließlich Kamera-Gruppen und Einstellungen."
|
||||
},
|
||||
"review": {
|
||||
"label": "In der Überprüfung anzeigen",
|
||||
|
||||
@@ -125,5 +125,7 @@
|
||||
"baby": "Baby",
|
||||
"baby_stroller": "Kinderwagen",
|
||||
"rickshaw": "Rikscha",
|
||||
"rodent": "Nagetier"
|
||||
"rodent": "Nagetier",
|
||||
"garbage_truck": "Müllfahrzeug",
|
||||
"possum": "Possum"
|
||||
}
|
||||
|
||||
@@ -64,7 +64,20 @@
|
||||
"title": "Klassifikationsmodell bearbeiten",
|
||||
"descriptionState": "Bearbeite die Klassen für dieses Zustandsklassifikationsmodell. Änderungen erfordern ein erneutes Trainieren des Modells.",
|
||||
"descriptionObject": "Bearbeite den Objekttyp und Klassifizierungstyp für dieses Objektklassifikationsmodell.",
|
||||
"stateClassesInfo": "Hinweis: Die Änderung der Statusklassen erfordert ein erneutes Trainieren des Modells mit den aktualisierten Klassen."
|
||||
"stateClassesInfo": "Modell aktualisiert. Trainieren Sie das Modell neu, damit die Klassenänderungen wirksam werden.",
|
||||
"enabled": "Aktiviert",
|
||||
"enabledDesc": "Dieses Modell ausführen. Bei Deaktivierung wird es nicht mehr ausgeführt und führt keine Klassifizierung mehr durch.",
|
||||
"saveAttempts": "Versuche speichern",
|
||||
"saveAttemptsDesc": "Anzahl der für die Übersicht der letzten Klassifizierungen zu speichernden Bilder.",
|
||||
"motion": "Bei Bewegung ausführen",
|
||||
"motionDesc": "Klassifizierung bei Bewegung im konfigurierten Bildausschnitt ausführen.",
|
||||
"interval": "Intervall",
|
||||
"intervalDesc": "Sekundenabstand zwischen regelmäßigen Klassifizierungsläufen. Leer lassen, um die Klassifizierung nur bei Bewegung auszuführen.",
|
||||
"intervalPlaceholder": "Kein Intervall",
|
||||
"errors": {
|
||||
"saveAttemptsInvalid": "Sparversuche müssen eine ganze Zahl von 0 oder mehr sein",
|
||||
"intervalInvalid": "Intervall muss eine ganze Nummer sein größer als 0"
|
||||
}
|
||||
},
|
||||
"deleteDatasetImages": {
|
||||
"title": "Datensatz Bilder löschen",
|
||||
@@ -195,5 +208,6 @@
|
||||
},
|
||||
"none": "Keiner",
|
||||
"reclassifyImageAs": "Bild neu klassifizieren als:",
|
||||
"reclassifyImage": "Bild neu klassifizieren"
|
||||
"reclassifyImage": "Bild neu klassifizieren",
|
||||
"disabled": "Deaktiviert"
|
||||
}
|
||||
|
||||
@@ -570,7 +570,7 @@
|
||||
"openCameraWebUI": "Web-Benutzeroberfläche von {{camera}} öffnen",
|
||||
"audio": {
|
||||
"title": "Audio",
|
||||
"noAudioDetections": "Keine Audioerkennungen",
|
||||
"noAudioDetections": "Kein Audio-Ereignis erkannt",
|
||||
"score": "Punktzahl",
|
||||
"currentRMS": "Aktueller Effektivwert",
|
||||
"currentdbFS": "Aktuelle dbFS"
|
||||
@@ -1383,7 +1383,7 @@
|
||||
"webuiUrlHelp": "URL, um die Web-Benutzeroberfläche der Kamera direkt aus der Debug-Ansicht aufzurufen. Lassen Sie das Feld leer, um den Link zu deaktivieren.",
|
||||
"webuiUrlInvalid": "Es muss sich um eine gültige URL handeln (z. B. https://example.com).",
|
||||
"dashboardLabel": "Im Live-Dashboard anzeigen",
|
||||
"dashboardHelp": "Diese Kamera im Live-Dashboard anzeigen.",
|
||||
"dashboardHelp": "Zeige diese Kamera auf dem Standard Alle Kameras live dashboard. Es bleibt überall verfügbar, einschließlich Kameragruppen.",
|
||||
"reviewLabel": "In der Überprüfung anzeigen",
|
||||
"reviewHelp": "Zeige diese Kamera in der Übersicht an, einschließlich des Kamerafilters, der Bewegungsübersicht und der Verlaufsansicht."
|
||||
},
|
||||
@@ -1430,7 +1430,7 @@
|
||||
"deleteCameraDialog": {
|
||||
"title": "Kamera löschen",
|
||||
"description": "Durch das Löschen einer Kamera werden alle Aufzeichnungen, erfassten Objekte und Konfigurationseinstellungen für diese Kamera endgültig entfernt. Alle mit dieser Kamera verbundenen go2rtc-Streams müssen möglicherweise noch manuell entfernt werden.",
|
||||
"selectPlaceholder": "Kamera auswählen...",
|
||||
"selectPlaceholder": "Kamera auswählen…",
|
||||
"confirmTitle": "Bist du dir sicher?",
|
||||
"confirmWarning": "Das Löschen von <strong>{{cameraName}}</strong> kann nicht rückgängig gemacht werden.",
|
||||
"deleteExports": "Lösche auch die Exporte für diese Kamera",
|
||||
@@ -1720,7 +1720,14 @@
|
||||
"keyLabel": "Schlüssel",
|
||||
"valueLabel": "Wert",
|
||||
"keyPlaceholder": "Neuer Schlüssel",
|
||||
"remove": "Entfernen"
|
||||
"remove": "Entfernen",
|
||||
"providerNameLabel": "Name des Anbieters",
|
||||
"providerNamePlaceholder": "z.B. openai",
|
||||
"variableNameLabel": "Variabler Name",
|
||||
"variableNamePlaceholder": "z.B. MY_VARIABLE",
|
||||
"loggerNameLabel": "Name des Loggers",
|
||||
"loggerNamePlaceholder": "z.B. frigate.record",
|
||||
"keyPatternError": "Verwenden Sie nur Buchstaben, Zahlen, Bindestriche und Unterstriche (keine Leerzeichen)"
|
||||
},
|
||||
"timezone": {
|
||||
"defaultOption": "Zeitzone des Browsers verwenden"
|
||||
|
||||
@@ -74,7 +74,7 @@
|
||||
"menuItem": "View motion previews",
|
||||
"title": "Motion previews: {{camera}}",
|
||||
"mobileSettingsTitle": "Motion Preview Settings",
|
||||
"mobileSettingsDesc": "Adjust playback speed and dimming, and choose a date to review motion-only clips.",
|
||||
"mobileSettingsDesc": "Adjust playback speed, dimming, and cropping, and choose a date to review motion-only clips.",
|
||||
"dim": "Dim",
|
||||
"dimAria": "Adjust dimming intensity",
|
||||
"dimDesc": "Increase dimming to increase motion area visibility.",
|
||||
@@ -87,6 +87,9 @@
|
||||
"seekAria": "Seek {{camera}} player to {{time}}",
|
||||
"filter": "Filter",
|
||||
"filterDesc": "Select areas to only show clips with motion in those regions.",
|
||||
"filterClear": "Clear"
|
||||
"filterClear": "Clear",
|
||||
"crop": "Crop to filter",
|
||||
"cropAria": "Toggle cropping previews to the filtered areas",
|
||||
"cropDesc": "Zoom previews into the selected filter areas instead of showing the full frame."
|
||||
}
|
||||
}
|
||||
|
||||
@@ -463,7 +463,7 @@
|
||||
"deleteCameraDialog": {
|
||||
"title": "Delete Camera",
|
||||
"description": "Deleting a camera will permanently remove all recordings, tracked objects, and configuration for that camera. Any go2rtc streams associated with this camera may still need to be manually removed.",
|
||||
"selectPlaceholder": "Choose camera...",
|
||||
"selectPlaceholder": "Choose camera…",
|
||||
"confirmTitle": "Are you sure?",
|
||||
"confirmWarning": "Deleting <strong>{{cameraName}}</strong> cannot be undone.",
|
||||
"deleteExports": "Also delete exports for this camera",
|
||||
@@ -613,7 +613,6 @@
|
||||
}
|
||||
},
|
||||
"footer": {
|
||||
"changeCount_zero": "No changes selected",
|
||||
"changeCount_one": "{{count}} change will be applied",
|
||||
"changeCount_other": "{{count}} changes will be applied",
|
||||
"restartNeeded": "Restart will be required for some changes.",
|
||||
|
||||
@@ -126,5 +126,7 @@
|
||||
"baby_stroller": "Lapsevanker",
|
||||
"rickshaw": "Rikša",
|
||||
"Rodent": "Näriline",
|
||||
"rodent": "Näriline"
|
||||
"rodent": "Näriline",
|
||||
"possum": "Opossum",
|
||||
"garbage_truck": "Prügiauto"
|
||||
}
|
||||
|
||||
@@ -74,7 +74,7 @@
|
||||
"menuItem": "Vaata liikumiste eelvaateid",
|
||||
"title": "Liikumiste eelvaated: {{camera}}",
|
||||
"mobileSettingsTitle": "Liikumiste eelvaadete seadistused",
|
||||
"mobileSettingsDesc": "Kohenda taasesituse kiirust ja heledust ning vali kuupäev, et vaadata läbi ainult liikumist kajastavaid klipid.",
|
||||
"mobileSettingsDesc": "Kohenda taasesituse kiirust, heledust ja kadreerimist ning vali kuupäev, et vaadata läbi ainult liikumist kajastavaid klipid.",
|
||||
"dim": "Hämarus",
|
||||
"dimAria": "Muuda hämarust",
|
||||
"dimDesc": "Kohenda hämarust parandamaks liikumisala nähtavust.",
|
||||
@@ -87,6 +87,9 @@
|
||||
"seekAria": "Keri „{{camera}}“ kaamera vaade ajatempli juurde: {{time}}",
|
||||
"filter": "Filtreeri",
|
||||
"filterDesc": "Näitamaks ainult liikumisega klippe antud aladel, vali soovitud piirkonnad.",
|
||||
"filterClear": "Tühjenda"
|
||||
"filterClear": "Tühjenda",
|
||||
"crop": "Kadreeri filtreerimiseks",
|
||||
"cropAria": "Lülita filtreeritud alade kadreeritud eelvaated sisse/välja",
|
||||
"cropDesc": "Täiskaadri kuvamise asemel suumi eelvaated valitud filtritega märgitud alani."
|
||||
}
|
||||
}
|
||||
|
||||
@@ -129,6 +129,9 @@
|
||||
"error": {
|
||||
"updatedLPRFailed": "Sõiduki numbrimärgi uuendamine ei õnnestunud: {{errorMessage}}"
|
||||
}
|
||||
},
|
||||
"button": {
|
||||
"share": "Jaga seda ülevaadatud objekti"
|
||||
}
|
||||
},
|
||||
"snapshotScore": {
|
||||
|
||||
@@ -538,7 +538,7 @@
|
||||
"licensePlateRecognition": {
|
||||
"title": "Sõidukite numbrimärkide tuvastus"
|
||||
},
|
||||
"title": "Andmerikastuse seaded",
|
||||
"title": "Andmerikastuse seadistused",
|
||||
"restart_required": "Taaskäivitamine on vajalik (Andmerikastuse seadeid on muudetud)",
|
||||
"toast": {
|
||||
"success": "Andmerikastamise seaded on salvestatud. Muudatuste rakendamiseks taaskäivitage Frigate.",
|
||||
|
||||
@@ -502,7 +502,7 @@
|
||||
},
|
||||
"model_type": {
|
||||
"label": "物体検知モデル種別",
|
||||
"description": "一部の検出器が最適化に使用する検知モデルのアーキテクチャ種別(ssd, yolox, yolonas)。"
|
||||
"description": "一部の検出器が最適化に使用する検知モデルのアーキテクチャ種別(ssd, yolox, yolonas, yolo-generic, rfdetr, dfine)。"
|
||||
}
|
||||
},
|
||||
"model_path": {
|
||||
@@ -675,7 +675,7 @@
|
||||
},
|
||||
"model_type": {
|
||||
"label": "物体検知モデル種別",
|
||||
"description": "一部の検出器が最適化に使用する検知モデルのアーキテクチャ種別(ssd, yolox, yolonas)。"
|
||||
"description": "一部の検出器が最適化に使用する検知モデルのアーキテクチャ種別(ssd, yolox, yolonas, yolo-generic, rfdetr, dfine)。"
|
||||
}
|
||||
},
|
||||
"genai": {
|
||||
@@ -1555,7 +1555,7 @@
|
||||
"description": "UI 内でのカメラの並び順に使用される数値(デフォルトダッシュボードとリスト)。値が大きいほど後ろに表示されます。"
|
||||
},
|
||||
"dashboard": {
|
||||
"label": "UI に表示",
|
||||
"label": "ライブダッシュボードに表示",
|
||||
"description": "このカメラを Frigate UI 全体に表示するかを切り替えます。無効化した場合、再表示するには設定ファイルを手動編集する必要があります。"
|
||||
},
|
||||
"review": {
|
||||
|
||||
@@ -147,7 +147,19 @@
|
||||
"deleteNow": "Trinti Dabar",
|
||||
"next": "Kitas",
|
||||
"off": "OFF",
|
||||
"continue": "Tęsti"
|
||||
"continue": "Tęsti",
|
||||
"add": "Pridėti",
|
||||
"applying": "Pritaikoma…",
|
||||
"undo": "Grąžinti",
|
||||
"copiedToClipboard": "Nukopijuotą į iškarpinę",
|
||||
"modified": "Modifikuota",
|
||||
"overridden": "Perrašytas",
|
||||
"resetToGlobal": "Sugąžinti į Globalius",
|
||||
"resetToDefault": "Sugrąžinti į Numatytuosius",
|
||||
"saveAll": "Išsaugoti Viską",
|
||||
"savingAll": "Viskas Išsaugoma…",
|
||||
"undoAll": "Grąžinti Viską",
|
||||
"retry": "Pakartoti"
|
||||
},
|
||||
"menu": {
|
||||
"system": "Sistema",
|
||||
@@ -199,7 +211,10 @@
|
||||
"bg": "Bulgarų",
|
||||
"gl": "Galician",
|
||||
"id": "Indonesian",
|
||||
"ur": "Urdu"
|
||||
"ur": "Urdu",
|
||||
"zhHant": "Tradicinė Kinijos",
|
||||
"hr": "Kroatų",
|
||||
"bs": "Bosnių"
|
||||
},
|
||||
"appearance": "Išvaizda",
|
||||
"darkMode": {
|
||||
@@ -249,7 +264,11 @@
|
||||
"setPassword": "Nustatyti Slaptažodi"
|
||||
},
|
||||
"uiPlayground": "UI Playground",
|
||||
"classification": "Klasifikavimas"
|
||||
"classification": "Klasifikavimas",
|
||||
"profiles": "Profiliai",
|
||||
"actions": "Veiksmai",
|
||||
"features": "Ypatybės",
|
||||
"chat": "Pokalbiai"
|
||||
},
|
||||
"toast": {
|
||||
"copyUrlToClipboard": "URL nukopijuotas į atmintį.",
|
||||
@@ -258,7 +277,8 @@
|
||||
"error": {
|
||||
"title": "Nepavyko išsaugoti konfiguracijos pakeitimų: {{errorMessage}}",
|
||||
"noMessage": "Nepavyko išsaugoti konfiguracijos pakeitimų"
|
||||
}
|
||||
},
|
||||
"success": "Konfiguracijos pokyčiai išsaugoti sėkmingai."
|
||||
}
|
||||
},
|
||||
"role": {
|
||||
@@ -302,5 +322,10 @@
|
||||
"field": {
|
||||
"optional": "Pasirinktinis",
|
||||
"internalID": "Vidinį ID Frigate naudoja konfiguracijoje ir duombazėje"
|
||||
},
|
||||
"no_items": "Nėra įrašų",
|
||||
"validation_errors": "Tikrinimo Klaidos",
|
||||
"credentialField": {
|
||||
"savedPlaceholder": "Išsaugota - palikite tuščia kad išliktų esamas"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -68,7 +68,10 @@
|
||||
}
|
||||
},
|
||||
"birdseye": "Birdseye"
|
||||
}
|
||||
},
|
||||
"showAll": "Rodyti visas kamerų grupes",
|
||||
"showLess": "Rodyti mažiau",
|
||||
"editGroups": "Koreguoti Kameru Grupes"
|
||||
},
|
||||
"debug": {
|
||||
"options": {
|
||||
@@ -82,6 +85,7 @@
|
||||
"zones": "Zonos",
|
||||
"mask": "Maskuotė",
|
||||
"motion": "Judesys",
|
||||
"regions": "Regionas"
|
||||
"regions": "Regionas",
|
||||
"paths": "Keliai"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,6 +20,9 @@
|
||||
},
|
||||
"state": {
|
||||
"submitted": "Pateikta"
|
||||
},
|
||||
"toast": {
|
||||
"error": "Pateikti į Frigate+ nepavyko. Prašau patikrinkite savo tinklo nustatymus ir bandykite dar kartą."
|
||||
}
|
||||
},
|
||||
"submitToPlus": {
|
||||
@@ -79,7 +82,16 @@
|
||||
"noVaildTimeSelected": "Nėra pasirinkto tinkamo laikotarpio"
|
||||
},
|
||||
"view": "Žiūrėti"
|
||||
}
|
||||
},
|
||||
"case": {
|
||||
"newCaseOption": "Kurti naują užklausą",
|
||||
"newCaseNamePlaceholder": "Naujos užklausos pavadinimas",
|
||||
"newCaseDescriptionPlaceholder": "Užklausos aprašymas",
|
||||
"label": "Užklausa",
|
||||
"nonAdminHelp": "Nauja užklausa bus sukurta šiems išrašams",
|
||||
"placeholder": "Pasirinkti užklausą"
|
||||
},
|
||||
"queueing": "Sudaroma išrašų eilė...."
|
||||
},
|
||||
"recording": {
|
||||
"button": {
|
||||
|
||||
@@ -28,7 +28,8 @@
|
||||
"triggers": "Trigeriai",
|
||||
"roles": "Rolės",
|
||||
"cameraManagement": "Valdymas",
|
||||
"cameraReview": "Peržiūra"
|
||||
"cameraReview": "Peržiūra",
|
||||
"profiles": "Profiliai"
|
||||
},
|
||||
"dialog": {
|
||||
"unsavedChanges": {
|
||||
|
||||
@@ -147,7 +147,7 @@
|
||||
"streaming": {
|
||||
"label": "Strøm",
|
||||
"restreaming": {
|
||||
"disabled": "Restrømming er ikke aktivert for dette kameraet.",
|
||||
"disabled": "Viderestrømming er ikke aktivert for dette kameraet.",
|
||||
"desc": {
|
||||
"readTheDocumentation": "Se dokumentasjonen",
|
||||
"title": "Konfigurer go2rtc for flere direktestrømmingsalternativer og lyd for dette kameraet."
|
||||
|
||||
@@ -235,7 +235,7 @@
|
||||
"label": "Direkteavspilling",
|
||||
"streams": {
|
||||
"label": "Navn på direktestrømmer",
|
||||
"description": "Kobling mellom konfigurerte strøm-navn og restream/go2rtc-navn brukt for direkteavspilling."
|
||||
"description": "Kobling mellom konfigurerte strøm-navn og viderestrøm/go2rtc-navn brukt for direkteavspilling."
|
||||
},
|
||||
"height": {
|
||||
"label": "Direktevisningshøyde",
|
||||
|
||||
@@ -81,7 +81,7 @@
|
||||
},
|
||||
"go2rtc": {
|
||||
"label": "go2rtc",
|
||||
"description": "Innstillinger for den integrerte go2rtc-tjenesten for videreformidling og oversettelse av direktestrømmer."
|
||||
"description": "Innstillinger for den integrerte go2rtc-tjenesten for viderestrømming og oversettelse av direktestrømmer."
|
||||
},
|
||||
"mqtt": {
|
||||
"label": "MQTT",
|
||||
@@ -586,7 +586,7 @@
|
||||
"description": "Modus for å inkludere kameraer i Fugleperspektiv: 'objects', 'motion' eller 'continuous'."
|
||||
},
|
||||
"restream": {
|
||||
"label": "Videreformidle RTSP",
|
||||
"label": "Viderestrøm RTSP",
|
||||
"description": "Videreformidle Fugleperspektiv-utdataen som en RTSP-strøm; aktivering av dette vil holde Fugleperspektiv kjørende kontinuerlig."
|
||||
},
|
||||
"width": {
|
||||
@@ -756,10 +756,10 @@
|
||||
},
|
||||
"live": {
|
||||
"label": "Direkteavspilling",
|
||||
"description": "Innstillinger for å kontrollere oppløsning og kvalitet på jsmpeg-direktestrømmer. Dette påvirker ikke kameraer som bruker go2rtc for direktevisning.",
|
||||
"description": "Innstillinger for å kontrollere oppløsning og kvalitet på jsmpeg-direktestrømmer. Dette påvirker ikke vderestrømmede kameraer som bruker go2rtc for direktevisning.",
|
||||
"streams": {
|
||||
"label": "Navn på direktestrømmer",
|
||||
"description": "Kobling mellom konfigurerte strøm-navn og restream/go2rtc-navn brukt for direkteavspilling."
|
||||
"description": "Kobling mellom konfigurerte strøm-navn og viderestrøm/go2rtc-navn brukt for direkteavspilling."
|
||||
},
|
||||
"height": {
|
||||
"label": "Direktevisningshøyde",
|
||||
|
||||
@@ -126,5 +126,7 @@
|
||||
"Rodent": "Gnager",
|
||||
"baby": "Baby",
|
||||
"rickshaw": "Rickshaw",
|
||||
"rodent": "Gnager"
|
||||
"rodent": "Gnager",
|
||||
"possum": "Pungrotte",
|
||||
"garbage_truck": "Avfallsbil"
|
||||
}
|
||||
|
||||
@@ -188,9 +188,23 @@
|
||||
"title": "Rediger klassifiseringsmodell",
|
||||
"descriptionState": "Rediger klassene for denne tilstandsklassifiseringsmodellen. Endringer vil kreve at modellen trenes på nytt.",
|
||||
"descriptionObject": "Rediger objekttypen og klassifiseringstypen for denne objektklassifiseringsmodellen.",
|
||||
"stateClassesInfo": "Modellen ble oppdatert. Tren modellen på nytt for at endringene i klassene skal bli iverksatt."
|
||||
"stateClassesInfo": "Modellen ble oppdatert. Tren modellen på nytt for at endringene i klassene skal bli iverksatt.",
|
||||
"enabled": "Aktivert",
|
||||
"enabledDesc": "Kjør denne modellen. Når den er deaktivert, stopper den og vil ikke lenger klassifisere.",
|
||||
"saveAttempts": "Lagringsforsøk",
|
||||
"saveAttemptsDesc": "Antall bilder fra klassifiseringsforsøk som skal lagres for visning av nylige klassifiseringer.",
|
||||
"motion": "Utfør ved bevegelse",
|
||||
"motionDesc": "Utfør klassifisering når bevegelse registreres innenfor det konfigurerte utsnittet.",
|
||||
"interval": "Intervall",
|
||||
"intervalDesc": "Sekunder mellom periodiske klassifiseringskjøringer. La stå tom for å kun kjøre ved bevegelse.",
|
||||
"intervalPlaceholder": "Ingen intervall",
|
||||
"errors": {
|
||||
"saveAttemptsInvalid": "Lagringsforsøk må være et heltall på 0 eller høyere",
|
||||
"intervalInvalid": "Intervall må være et heltall større enn 0"
|
||||
}
|
||||
},
|
||||
"none": "Ingen",
|
||||
"reclassifyImageAs": "Reklassifiser bilde som:",
|
||||
"reclassifyImage": "Reklassifiser bilde"
|
||||
"reclassifyImage": "Reklassifiser bilde",
|
||||
"disabled": "Deaktivert"
|
||||
}
|
||||
|
||||
@@ -1305,7 +1305,7 @@
|
||||
},
|
||||
"featuresPopover": {
|
||||
"title": "Strømfunksjoner",
|
||||
"description": "Bruk go2rtc-restrømming for å redusere antall tilkoblinger til kameraet."
|
||||
"description": "Bruk go2rtc-viderestrømming for å redusere antall tilkoblinger til kameraet."
|
||||
}
|
||||
},
|
||||
"step4": {
|
||||
@@ -1382,7 +1382,7 @@
|
||||
"saving": "Lagrer…",
|
||||
"saved": "Lagret",
|
||||
"label": "Kamerastatus",
|
||||
"description": "Angi driftsstatus for hvert kamera.<br /><br /><strong>På</strong>: Strømmer behandles normalt.<br /><strong>Av</strong>: Setter behandlingen midlertidig på pause. Vedvarer ikke etter omstart av Frigate.<br /><strong>Deaktivert</strong>: Stopper behandlingen og lagrer endringen i konfigurasjonen din. En omstart kreves for å aktivere et deaktivert kamera igjen.<br /><br /><em>Merk: Deaktivering påvirker ikke videreformidling (restream) i go2rtc.</em><br /><br />Dra i ikonet for å endre rekkefølgen på aktive kameraer slik de vises i grensesnittet, inkludert Live-dashbordet og rullegardinmenyene for kameravalg.",
|
||||
"description": "Angi driftsstatus for hvert kamera.<br /><br /><strong>På</strong>: Strømmer behandles normalt.<br /><strong>Av</strong>: Setter behandlingen midlertidig på pause. Vedvarer ikke etter omstart av Frigate.<br /><strong>Deaktivert</strong>: Stopper behandlingen og lagrer endringen i konfigurasjonen din. En omstart kreves for å aktivere et deaktivert kamera igjen.<br /><br /><em>Merk: Deaktivering påvirker ikke viderestrømming (restream) i go2rtc.</em><br /><br />Dra i ikonet for å endre rekkefølgen på aktive kameraer slik de vises i grensesnittet, inkludert Live-dashbordet og rullegardinmenyene for kameravalg.",
|
||||
"disabledSubheading": "Deaktivert i konfigurasjonen",
|
||||
"status": {
|
||||
"on": "På",
|
||||
@@ -1670,8 +1670,8 @@
|
||||
"preset-rtmp-generic": "RTMP (Generisk)",
|
||||
"preset-rtsp-blue-iris": "RTSP - Blue Iris",
|
||||
"preset-rtsp-udp": "RTSP - UDP",
|
||||
"preset-rtsp-restream": "RTSP - Videreformidling fra go2rtc",
|
||||
"preset-rtsp-restream-low-latency": "RTSP - Videreformidling fra go2rtc (lav forsinkelse)",
|
||||
"preset-rtsp-restream": "RTSP - Viderestrømming fra go2rtc",
|
||||
"preset-rtsp-restream-low-latency": "RTSP - Viderestrømming fra go2rtc (lav forsinkelse)",
|
||||
"preset-rtsp-generic": "RTSP (Generisk)",
|
||||
"preset-vaapi": "VAAPI (Intel/AMD GPU)"
|
||||
},
|
||||
@@ -1725,7 +1725,11 @@
|
||||
"valueLabel": "Verdi",
|
||||
"providerNamePlaceholder": "f.eks. openai",
|
||||
"variableNameLabel": "Variabelnavn",
|
||||
"variableNamePlaceholder": "f.eks MIN_VARIABEL"
|
||||
"variableNamePlaceholder": "f.eks MIN_VARIABEL",
|
||||
"providerNameLabel": "Tilbydernavn",
|
||||
"loggerNameLabel": "Loggernavn",
|
||||
"loggerNamePlaceholder": "f.eks. frigate.record",
|
||||
"keyPatternError": "Bruk kun bokstaver, tall, bindestreker og understreker (ingen mellomrom)"
|
||||
},
|
||||
"roleMap": {
|
||||
"remove": "Fjern",
|
||||
@@ -1775,7 +1779,7 @@
|
||||
"cameraInputs": {
|
||||
"itemTitle": "Strøm {{index}}",
|
||||
"sourceMode": {
|
||||
"restream": "Videreformidling / Restream (go2rtc)",
|
||||
"restream": "Viderestrøm (go2rtc)",
|
||||
"manual": "Manuell kildesti",
|
||||
"go2rtcStreamLabel": "go2rtc-strøm",
|
||||
"go2rtcStreamPlaceholder": "Velg en go2rtc-strøm",
|
||||
@@ -1860,7 +1864,7 @@
|
||||
}
|
||||
},
|
||||
"go2rtcStreams": {
|
||||
"description": "Administrer go2rtc-strømkonfigurasjoner for videreformidling av kamera-strømmer. Hver strøm har ett navn og én eller flere kilde-URL-er.",
|
||||
"description": "Administrer go2rtc-strømkonfigurasjoner for viderestrømming av kamera-strømmer. Hver strøm har ett navn og én eller flere kilde-URL-er.",
|
||||
"ffmpeg": {
|
||||
"hardwareAuto": "Automatisk (anbefalt)",
|
||||
"useFfmpegModule": "Bruk kompatibilitetsmodus (ffmpeg)",
|
||||
@@ -2010,7 +2014,8 @@
|
||||
"inputDimensionsNotDetectResolution": "Modellens inndata for bredde og høyde er dimensjonene til selve objektdeteksjonsmodellen, ikke kameraets deteksjonsoppløsning. De skal samsvare med dimensjonene til modellen du bruker – vanligvis en kvadratisk størrelse som 320x320 eller 640x640."
|
||||
},
|
||||
"ffmpeg": {
|
||||
"hwaccelManualNotRecommended": "Manuelle argumenter for maskinvareakselerasjon anbefales ikke. Med mindre det foreligger et spesifikt behov, bør du velge forhåndsinnstillingen som samsvarer med maskinvaren din."
|
||||
"hwaccelManualNotRecommended": "Manuelle argumenter for maskinvareakselerasjon anbefales ikke. Med mindre det foreligger et spesifikt behov, bør du velge forhåndsinnstillingen som samsvarer med maskinvaren din.",
|
||||
"inputsMissingGo2rtcStream": "En innstrøm nedenfor peker til en go2rtc-viderestrøm som ikke lenger eksisterer. Velg en eksisterende viderestrømeller oppgi kameraets URL manuelt, ellers vil ikke kameraet klarer å koble til."
|
||||
},
|
||||
"onvif": {
|
||||
"autotrackingNoZones": "Autosporing krever minst én sone. Definer en sone for dette kameraet under Masker / Soner, og angi den deretter som en påkrevd sone nedenfor."
|
||||
|
||||
@@ -65,13 +65,16 @@
|
||||
"select": "Selecteer",
|
||||
"toast": {
|
||||
"error": {
|
||||
"failed": "Exporteren is mislukt: {{error}}",
|
||||
"failed": "Exporter naar wachtrij mislukt: {{error}}",
|
||||
"noVaildTimeSelected": "Geen geldig tijdsbereik geselecteerd",
|
||||
"endTimeMustAfterStartTime": "Eindtijd moet na starttijd zijn"
|
||||
"endTimeMustAfterStartTime": "Eindtijd moet na starttijd zijn",
|
||||
"noValidTimeSelected": "Geen geldige tijd voor geselecteerd bereik"
|
||||
},
|
||||
"success": "Export is succesvol gestart. Bekijk het bestand op de exportpagina.",
|
||||
"view": "Weergeven",
|
||||
"queued": "Export staat in de wachtrij. Bekijk voortgang op de exports pagina."
|
||||
"queued": "Export staat in de wachtrij. Bekijk voortgang op de exports pagina.",
|
||||
"batchSuccess_one": "1 export gestart. Zaak wordt aangemaakt.",
|
||||
"batchSuccess_other": "{{count}} export gestart. Zaak wordt aangemaakt."
|
||||
},
|
||||
"fromTimeline": {
|
||||
"saveExport": "Export opslaan",
|
||||
@@ -125,7 +128,9 @@
|
||||
"startedNoCase_one": "1 export gestart.",
|
||||
"startedNoCase_other": "{{count}} Exports gestart.",
|
||||
"partial": "{{successful}} van {{total}} exports gestart. Mislukte items: {{failedItems}}",
|
||||
"failed": "Het starten van {{total}} exports is mislukt. Mislukt: {{failedItems}}"
|
||||
"failed": "Het starten van {{total}} exports is mislukt. Mislukt: {{failedItems}}",
|
||||
"started_one": "1 export gestart. Zaak wordt aangemaakt.",
|
||||
"started_other": "{{count}} exports gestart. Zaak wordt aangemaakt."
|
||||
},
|
||||
"exportingButton": "Exporteren...",
|
||||
"exportButton_one": "Exporteer 1 revisie",
|
||||
|
||||
@@ -867,8 +867,8 @@
|
||||
"description": "Ordine numerică folosită pentru sortarea camerei în interfață (dashboard și liste); numerele mai mari apar mai târziu."
|
||||
},
|
||||
"dashboard": {
|
||||
"label": "Arată în interfață",
|
||||
"description": "Comută vizibilitatea acestei camere peste tot în interfața Frigate. Dezactivarea acestei opțiuni va necesita editarea manuală a configurației pentru a vedea din nou camera în interfață."
|
||||
"label": "Afișează pe tabloul de bord Live",
|
||||
"description": "Comută dacă această cameră este vizibilă pe tabloul de bord Live implicit „Toate camerele”. Camera rămâne disponibilă în restul interfeței, inclusiv în grupurile de camere și în setări."
|
||||
},
|
||||
"review": {
|
||||
"label": "Arată în Revizuire",
|
||||
|
||||
@@ -2338,8 +2338,8 @@
|
||||
"description": "Ordinea numerică folosită pentru sortarea camerei în interfață (dashboard-ul implicit și liste); numerele mai mari apar mai târziu."
|
||||
},
|
||||
"dashboard": {
|
||||
"label": "Arată în interfață",
|
||||
"description": "Comută dacă această cameră este vizibilă peste tot în interfața Frigate. Dezactivarea acestei opțiuni va necesita editarea manuală a config-ului pentru a vedea din nou camera în interfață."
|
||||
"label": "Afișează pe tabloul de bord Live",
|
||||
"description": "Comută dacă această cameră este vizibilă pe tabloul de bord Live implicit „Toate camerele”. Camera rămâne disponibilă în restul interfeței, inclusiv în grupurile de camere și în setări."
|
||||
},
|
||||
"review": {
|
||||
"label": "Arată în Revizuire",
|
||||
|
||||
@@ -126,5 +126,7 @@
|
||||
"baby_stroller": "Cărucior de copii",
|
||||
"rickshaw": "Ricșă",
|
||||
"Rodent": "Rozătoare",
|
||||
"rodent": "Rozătoare"
|
||||
"rodent": "Rozătoare",
|
||||
"garbage_truck": "Mașină de gunoi",
|
||||
"possum": "Oposum"
|
||||
}
|
||||
|
||||
@@ -76,7 +76,7 @@
|
||||
"menuItem": "Vezi previzualizări mișcare",
|
||||
"title": "Previzualizări mișcare: {{camera}}",
|
||||
"mobileSettingsTitle": "Setări previzualizări mișcare",
|
||||
"mobileSettingsDesc": "Ajustează viteza de redare și estomparea, și alege o dată pentru a revizui clipurile doar cu mișcare.",
|
||||
"mobileSettingsDesc": "Ajustează viteza de redare, întunecarea și decuparea și alege o dată pentru a examina clipurile doar cu mișcare.",
|
||||
"dim": "Estompare",
|
||||
"dimAria": "Ajustează intensitatea estompării",
|
||||
"dimDesc": "Crește intensitatea estompării pentru a mări vizibilitatea zonei de mișcare.",
|
||||
@@ -89,6 +89,9 @@
|
||||
"seekAria": "Derulează player-ul {{camera}} la {{time}}",
|
||||
"filter": "Filtru",
|
||||
"filterDesc": "Selectează zonele pentru a afișa doar clipurile cu mișcare în acele regiuni.",
|
||||
"filterClear": "Șterge"
|
||||
"filterClear": "Șterge",
|
||||
"crop": "Decupează după filtru",
|
||||
"cropDesc": "Mărește previzualizările pe zonele de filtrare selectate în loc să afișeze cadrul complet.",
|
||||
"cropAria": "Comută decuparea previzualizărilor pe zonele filtrate"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1333,7 +1333,7 @@
|
||||
"webuiUrlInvalid": "Trebuie să fie un URL valid (de exemplu, https://exemplu.com).",
|
||||
"dashboardLabel": "Arată pe dashboard-ul Live",
|
||||
"reviewLabel": "Arată în Revizuire",
|
||||
"dashboardHelp": "Arată această cameră pe dashboard-ul Live.",
|
||||
"dashboardHelp": "Afișează această cameră pe tabloul de bord Live implicit „Toate camerele”. Aceasta rămâne disponibilă în restul interfeței, inclusiv în grupurile de camere.",
|
||||
"reviewHelp": "Arată această cameră în revizuiri, inclusiv filtrul de camere, revizuirea mișcărilor și vizualizarea istoricului."
|
||||
},
|
||||
"label": "Stare cameră",
|
||||
@@ -1379,7 +1379,7 @@
|
||||
"deleteCameraDialog": {
|
||||
"title": "Șterge camera",
|
||||
"description": "Ștergerea unei camere va elimina definitiv toate înregistrările, obiectele urmărite și configurația pentru acea cameră. Orice fluxuri go2rtc asociate cu această cameră ar putea necesita în continuare eliminarea manuală.",
|
||||
"selectPlaceholder": "Alege camera...",
|
||||
"selectPlaceholder": "Alege camera…",
|
||||
"confirmTitle": "Ești sigur?",
|
||||
"confirmWarning": "Ștergerea <strong>{{cameraName}}</strong> nu poate fi anulată.",
|
||||
"deleteExports": "Șterge și exporturile pentru această cameră",
|
||||
|
||||
@@ -95,7 +95,7 @@
|
||||
"placeholder": "Выберите случай",
|
||||
"newCaseOption": "Создайте новый случай",
|
||||
"newCaseNamePlaceholder": "Имя нового случая",
|
||||
"newCaseDescriptionPlaceholder": "Описание Случая",
|
||||
"newCaseDescriptionPlaceholder": "Описание случая",
|
||||
"nonAdminHelp": "Для этих экспортов будет создан новый случай."
|
||||
},
|
||||
"queueing": "Помещаем Экспорт в очередь...",
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
},
|
||||
"label": "Конфигурация",
|
||||
"audio": {
|
||||
"label": "Аудиособытия",
|
||||
"label": "Обнаружение звука",
|
||||
"description": "Настройки обнаружения аудиособытий для этой камеры.",
|
||||
"enabled": {
|
||||
"label": "Включить обнаружение звука",
|
||||
@@ -28,12 +28,16 @@
|
||||
"description": "Для запуска функции обнаружения звука требуется минимальный пороговый уровень громкости RMS; более низкие значения повышают чувствительность (например, 200 — высокий, 500 — средний, 1000 — низкий)."
|
||||
},
|
||||
"listen": {
|
||||
"description": "Список типов аудиособытий для обнаружения (например: лай, пожарная тревога, крик, речь, вопль).",
|
||||
"description": "Список типов аудиособытий для обнаружения (например: лай, пожарная тревога, речь, вопль).",
|
||||
"label": "Типы аудиособытий"
|
||||
},
|
||||
"filters": {
|
||||
"label": "Аудиофильтры",
|
||||
"description": "Настройки фильтров для каждого типа аудиофайлов, такие как пороговые значения, используются для уменьшения количества ложных срабатываний."
|
||||
"description": "Настройки фильтров для каждого типа аудиофайлов, такие как пороговые значения, используются для уменьшения количества ложных срабатываний.",
|
||||
"threshold": {
|
||||
"label": "Минимальная уверенность в звуке",
|
||||
"description": "Минимальный порог достоверности для подсчета аудиособытия."
|
||||
}
|
||||
},
|
||||
"enabled_in_config": {
|
||||
"label": "Исходное состояние звука",
|
||||
@@ -104,7 +108,693 @@
|
||||
},
|
||||
"stationary": {
|
||||
"label": "Конфигурация стационарных объектов",
|
||||
"description": "Настройки для обнаружения и управления объектами, которые остаются неподвижными в течение определенного периода времени."
|
||||
"description": "Настройки для обнаружения и управления объектами, которые остаются неподвижными в течение определенного периода времени.",
|
||||
"interval": {
|
||||
"label": "Стационарный интервал",
|
||||
"description": "Как часто (в кадрах) запускать проверку обнаружения для подтверждения неподвижного объекта."
|
||||
},
|
||||
"threshold": {
|
||||
"label": "Стационарный порог",
|
||||
"description": "Количество кадров без изменения положения, необходимое для того, чтобы пометить объект как неподвижный."
|
||||
},
|
||||
"max_frames": {
|
||||
"label": "Максимальное количество кадров",
|
||||
"description": "Ограничивает продолжительность отслеживания стационарных объектов, прежде чем они будут отброшены.",
|
||||
"default": {
|
||||
"label": "Максимальное количество кадров по умолчанию",
|
||||
"description": "Максимальное количество кадров по умолчанию для отслеживания неподвижного объекта перед остановкой."
|
||||
},
|
||||
"objects": {
|
||||
"label": "Максимальное количество кадров объекта",
|
||||
"description": "Переопределения для каждого объекта для максимального количества кадров для отслеживания неподвижных объектов."
|
||||
}
|
||||
},
|
||||
"classifier": {
|
||||
"label": "Включить визуальный классификатор",
|
||||
"description": "Используйте визуальный классификатор для обнаружения действительно неподвижных объектов даже при дрожании ограничивающих рамок."
|
||||
}
|
||||
},
|
||||
"annotation_offset": {
|
||||
"label": "Смещение аннотации",
|
||||
"description": "Миллисекунды для обнаружения смещения аннотаций, чтобы лучше совместить ограничивающие рамки временной шкалы с записями; может быть положительным или отрицательным."
|
||||
}
|
||||
},
|
||||
"mqtt": {
|
||||
"label": "MQTT"
|
||||
},
|
||||
"notifications": {
|
||||
"label": "Уведомления",
|
||||
"enabled": {
|
||||
"label": "Включить уведомления"
|
||||
},
|
||||
"email": {
|
||||
"label": "Уведомление по электронной почте",
|
||||
"description": "Адрес электронной почты, используемый для push-уведомлений или требуемый некоторыми поставщиками уведомлений."
|
||||
},
|
||||
"cooldown": {
|
||||
"label": "Период охлаждения",
|
||||
"description": "Пауза (в секундах) между уведомлениями, чтобы не спамить получателей."
|
||||
},
|
||||
"enabled_in_config": {
|
||||
"label": "Исходное состояние уведомлений",
|
||||
"description": "Указывает, были ли уведомления включены в исходной статической конфигурации."
|
||||
}
|
||||
},
|
||||
"ffmpeg": {
|
||||
"path": {
|
||||
"label": "Путь FFmpeg",
|
||||
"description": "Путь к используемому двоичному файлу FFmpeg или псевдоним версии («7.0» или «8.0»)."
|
||||
},
|
||||
"global_args": {
|
||||
"label": "Глобальные аргументы FFmpeg",
|
||||
"description": "Глобальные аргументы, передаваемые процессам FFmpeg."
|
||||
},
|
||||
"hwaccel_args": {
|
||||
"label": "Аргументы аппаратного ускорения",
|
||||
"description": "Аргументы аппаратного ускорения для FFmpeg. Рекомендуется использовать предустановки, зависящие от поставщика."
|
||||
},
|
||||
"input_args": {
|
||||
"label": "Входные аргументы",
|
||||
"description": "Входные аргументы, применяемые к входным потокам FFmpeg."
|
||||
},
|
||||
"output_args": {
|
||||
"label": "Выходные аргументы",
|
||||
"description": "Выходные аргументы по умолчанию, используемые для различных ролей FFmpeg, таких как обнаружение и запись.",
|
||||
"detect": {
|
||||
"label": "Выходные аргументы обнаружения",
|
||||
"description": "Выходные аргументы по умолчанию для потоков роли обнуружения."
|
||||
},
|
||||
"record": {
|
||||
"label": "Выходные аргументы записи",
|
||||
"description": "Выходные аргументы по умолчанию для потоков роли запись."
|
||||
}
|
||||
},
|
||||
"retry_interval": {
|
||||
"label": "Интервал повтора FFmpeg",
|
||||
"description": "Секунды ожидания перед повторным подключением потока камеры после сбоя. По умолчанию 10."
|
||||
},
|
||||
"apple_compatibility": {
|
||||
"label": "Совместимость с Apple",
|
||||
"description": "Включить тегирование HEVC для лучшей совместимости с плеерами Apple при записи H.265."
|
||||
},
|
||||
"gpu": {
|
||||
"label": "Индекс GPU",
|
||||
"description": "Индекс GPU по умолчанию, используемый для аппаратного ускорения, если он доступен."
|
||||
},
|
||||
"inputs": {
|
||||
"label": "Входы камеры",
|
||||
"description": "Список определений входного потока (пути и роли) для этой камеры.",
|
||||
"path": {
|
||||
"label": "Входной путь",
|
||||
"description": "URL или путь входного потока камеры."
|
||||
},
|
||||
"roles": {
|
||||
"label": "Входные роли",
|
||||
"description": "Роли для этого входного потока."
|
||||
},
|
||||
"global_args": {
|
||||
"label": "Глобальные аргументы FFmpeg",
|
||||
"description": "Глобальные аргументы FFmpeg для этого входного потока."
|
||||
},
|
||||
"hwaccel_args": {
|
||||
"label": "Аргументы аппаратного ускорения",
|
||||
"description": "Аргументы аппаратного ускорения для этого входного потока."
|
||||
},
|
||||
"input_args": {
|
||||
"label": "Входные аргументы",
|
||||
"description": "Входные аргументы, специфичные для этого потока."
|
||||
}
|
||||
}
|
||||
},
|
||||
"live": {
|
||||
"label": "Просмотр в реальном времени",
|
||||
"streams": {
|
||||
"label": "Имена live-потоков",
|
||||
"description": "Сопоставление настроенных имён потоков с именами restream/go2rtc для просмотра в реальном времени."
|
||||
},
|
||||
"height": {
|
||||
"label": "Высота live-потока",
|
||||
"description": "Высота (в пикселях) для отображения jsmpeg-потока в веб-интерфейсе; должна быть <= высоты потока обнаружения."
|
||||
},
|
||||
"quality": {
|
||||
"label": "Качество live-потока",
|
||||
"description": "Качество кодирования потока jsmpeg (1 — наивысшее, 31 — наинизшее)."
|
||||
}
|
||||
},
|
||||
"motion": {
|
||||
"label": "Обнаружение движения",
|
||||
"enabled": {
|
||||
"label": "Включить обнаружение движения"
|
||||
},
|
||||
"threshold": {
|
||||
"label": "Порог движения",
|
||||
"description": "Порог разницы пикселей, используемый детектором движения; более высокие значения снижают чувствительность (диапазон 1–255)."
|
||||
},
|
||||
"lightning_threshold": {
|
||||
"label": "Порог молнии",
|
||||
"description": "Порог для обнаружения и игнорирования кратковременных всплесков освещения (ниже - более чувствительно, значения от 0,3 до 1,0). Это не предотвращает полное обнаружение движения; это просто заставляет детектор прекратить анализ дополнительных кадров после превышения порога. Во время этих событий по-прежнему создаются записи на основе движения."
|
||||
},
|
||||
"skip_motion_threshold": {
|
||||
"label": "Порог пропуска движения",
|
||||
"description": "Если задано значение от 0.0 до 1.0 и за один кадр изменяется большая доля изображения, детектор не вернет области движения и сразу выполнит повторную калибровку. Это снижает нагрузку на CPU и число ложных срабатываний во время молний, гроз и т. п., но может пропустить реальные события, например автотрекинг объекта PTZ-камерой. Компромисс состоит в выборе между потерей нескольких мегабайт записи и просмотром пары коротких клипов. Оставьте параметр незаданным (None), чтобы отключить эту функцию."
|
||||
},
|
||||
"improve_contrast": {
|
||||
"label": "Улучшить контраст",
|
||||
"description": "Примените улучшение контрастности к кадрам перед анализом движения, чтобы облегчить обнаружение."
|
||||
},
|
||||
"contour_area": {
|
||||
"label": "Контурная площадь",
|
||||
"description": "Минимальная площадь контура в пикселях, необходимая для подсчета контура движения."
|
||||
},
|
||||
"delta_alpha": {
|
||||
"label": "Дельта альфа",
|
||||
"description": "Коэффициент альфа-смешивания, используемый при различении кадров для расчета движения."
|
||||
},
|
||||
"frame_alpha": {
|
||||
"label": "Кадр альфа",
|
||||
"description": "Значение альфа, используемое при смешивании кадров для предварительной обработки движения."
|
||||
},
|
||||
"frame_height": {
|
||||
"label": "Высота рамки",
|
||||
"description": "Высота в пикселях для масштабирования кадров при расчете движения."
|
||||
},
|
||||
"mask": {
|
||||
"label": "Координаты маски",
|
||||
"description": "Упорядоченные координаты x,y, определяющие многоугольник маски движения, используемый для включения/исключения областей."
|
||||
},
|
||||
"mqtt_off_delay": {
|
||||
"label": "MQTT задержка выключения",
|
||||
"description": "Секунды ожидания после последнего движения перед публикацией состояния «выключено» MQTT."
|
||||
},
|
||||
"enabled_in_config": {
|
||||
"label": "Исходное состояние движения",
|
||||
"description": "Указывает, было ли включено обнаружение движения в исходной статической конфигурации."
|
||||
},
|
||||
"raw_mask": {
|
||||
"label": "Исходная маска"
|
||||
}
|
||||
},
|
||||
"objects": {
|
||||
"label": "Объекты",
|
||||
"description": "Параметры отслеживания объектов по умолчанию, включая метки для отслеживания и фильтры для каждого объекта.",
|
||||
"track": {
|
||||
"label": "Объекты для отслеживания"
|
||||
},
|
||||
"filters": {
|
||||
"label": "Фильтры объектов",
|
||||
"description": "К обнаруженным объектам применяются фильтры для уменьшения ложных срабатываний (площадь, соотношение, достоверность).",
|
||||
"min_area": {
|
||||
"label": "Минимальная площадь объекта",
|
||||
"description": "Минимальная площадь ограничивающей рамки (в пикселях или процентах), необходимая для этого типа объекта. Может быть в пикселях (целое число) или в процентах (плавающее значение от 0,000001 до 0,99)."
|
||||
},
|
||||
"max_area": {
|
||||
"label": "Максимальная площадь объекта",
|
||||
"description": "Максимальная площадь ограничивающей рамки (в пикселях или процентах), разрешенная для этого типа объекта. Может быть в пикселях (целое число) или в процентах (плавающее значение от 0,000001 до 0,99)."
|
||||
},
|
||||
"min_ratio": {
|
||||
"label": "Минимальное соотношение сторон",
|
||||
"description": "Минимальное соотношение ширины и высоты, необходимое для соответствия ограничивающей рамки."
|
||||
},
|
||||
"max_ratio": {
|
||||
"label": "Максимальное соотношение сторон",
|
||||
"description": "Максимальное соотношение ширины и высоты, допускаемое для соответствия ограничивающей рамке."
|
||||
},
|
||||
"threshold": {
|
||||
"label": "Порог уверенности",
|
||||
"description": "Средний порог достоверности обнаружения, необходимый для того, чтобы объект считался истинно положительным."
|
||||
},
|
||||
"min_score": {
|
||||
"label": "Минимальная уверенность",
|
||||
"description": "Минимальная достоверность обнаружения одного кадра, необходимая для подсчета объекта."
|
||||
},
|
||||
"mask": {
|
||||
"label": "Маска фильтра",
|
||||
"description": "Координаты полигона, определяющие, где этот фильтр применяется в кадре."
|
||||
},
|
||||
"raw_mask": {
|
||||
"label": "Исходная маска"
|
||||
}
|
||||
},
|
||||
"mask": {
|
||||
"label": "Маска объекта",
|
||||
"description": "Полигон маски, используемый для предотвращения обнаружения объектов в определенных областях."
|
||||
},
|
||||
"raw_mask": {
|
||||
"label": "Исходная маска"
|
||||
},
|
||||
"genai": {
|
||||
"label": "Конфигурация объекта GenAI",
|
||||
"description": "Опции GenAI для описания отслеживаемых объектов и отправки кадров на генерацию.",
|
||||
"enabled": {
|
||||
"label": "Включить GenAI",
|
||||
"description": "По умолчанию включить GenAI генерацию описаний отслеживаемых объектов."
|
||||
},
|
||||
"use_snapshot": {
|
||||
"label": "Используйте снимки",
|
||||
"description": "Используйте снимки объектов вместо миниатюр для создания описания GenAI."
|
||||
},
|
||||
"prompt": {
|
||||
"label": "Промпт описания",
|
||||
"description": "Шаблон промпта по умолчанию для генерации описаний с помощью GenAI."
|
||||
},
|
||||
"object_prompts": {
|
||||
"label": "Промпты по объектам",
|
||||
"description": "Промпты для отдельных объектов, чтобы настроить вывод GenAI для конкретных меток."
|
||||
},
|
||||
"objects": {
|
||||
"label": "Объекты GenAI",
|
||||
"description": "Список меток объектов для отправки в GenAI по умолчанию."
|
||||
},
|
||||
"required_zones": {
|
||||
"label": "Обязательные зоны",
|
||||
"description": "Зоны, которые необходимо ввести для объектов, чтобы соответствовать критериям создания описания GenAI."
|
||||
},
|
||||
"debug_save_thumbnails": {
|
||||
"label": "Сохранить миниатюры",
|
||||
"description": "Сохранять миниатюры, отправляемые в GenAI, для отладки и просмотра."
|
||||
},
|
||||
"send_triggers": {
|
||||
"label": "Триггеры GenAI",
|
||||
"description": "Определяет, когда кадры должны отправляться на GenAI (по окончании, после обновлений и т. д.).",
|
||||
"tracked_object_end": {
|
||||
"label": "Отправить в конце",
|
||||
"description": "Отправьте запрос на GenAI, когда отслеживаемый объект закончится."
|
||||
},
|
||||
"after_significant_updates": {
|
||||
"label": "Ранний триггер GenAI",
|
||||
"description": "Отправьте запрос GenAI после указанного количества существенных обновлений отслеживаемого объекта."
|
||||
}
|
||||
},
|
||||
"enabled_in_config": {
|
||||
"label": "Исходное состояние GenAI",
|
||||
"description": "Указывает, был ли включен GenAI в исходной статической конфигурации."
|
||||
}
|
||||
}
|
||||
},
|
||||
"record": {
|
||||
"label": "Запись",
|
||||
"enabled": {
|
||||
"label": "Включить запись"
|
||||
},
|
||||
"expire_interval": {
|
||||
"label": "Запись интервала очистки",
|
||||
"description": "Минуты между проходами очистки, при которых удаляются просроченные сегменты записи."
|
||||
},
|
||||
"continuous": {
|
||||
"label": "Постоянное удержание",
|
||||
"description": "Количество дней для хранения записей независимо от отслеживаемых объектов или движения. Установите значение 0, если вы хотите сохранять только записи предупреждений и обнаружений.",
|
||||
"days": {
|
||||
"label": "Дни хранения",
|
||||
"description": "Дни хранения записей."
|
||||
}
|
||||
},
|
||||
"motion": {
|
||||
"label": "Удержание движения",
|
||||
"description": "Количество дней для хранения записей, вызванных движением, независимо от отслеживаемых объектов. Установите значение 0, если вы хотите сохранять только записи предупреждений и обнаружений.",
|
||||
"days": {
|
||||
"label": "Дни хранения",
|
||||
"description": "Дни хранения записей."
|
||||
}
|
||||
},
|
||||
"detections": {
|
||||
"label": "Сохранение обнаружения",
|
||||
"description": "Настройки хранения записи для событий обнаружения, включая продолжительность до и после захвата.",
|
||||
"pre_capture": {
|
||||
"label": "Предварительная съемка секунд",
|
||||
"description": "Количество секунд до события обнаружения, которое необходимо включить в запись."
|
||||
},
|
||||
"post_capture": {
|
||||
"label": "Секунды после захвата",
|
||||
"description": "Количество секунд после события обнаружения, которое необходимо включить в запись."
|
||||
},
|
||||
"retain": {
|
||||
"label": "Сохранение событий",
|
||||
"description": "Настройки хранения записей событий обнаружения.",
|
||||
"days": {
|
||||
"label": "Дни хранения",
|
||||
"description": "Количество дней для хранения записей событий обнаружения."
|
||||
},
|
||||
"mode": {
|
||||
"label": "Режим хранения",
|
||||
"description": "Режим сохранения: all (сохранить все сегменты), motion (сохранить сегменты с движением) или active_objects (сохранить сегменты с активными объектами)."
|
||||
}
|
||||
}
|
||||
},
|
||||
"alerts": {
|
||||
"label": "Сохранение оповещений",
|
||||
"description": "Настройки хранения записи для событий оповещений, включая продолжительность до и после захвата.",
|
||||
"pre_capture": {
|
||||
"label": "Предварительная съемка секунд",
|
||||
"description": "Количество секунд до события обнаружения, которое необходимо включить в запись."
|
||||
},
|
||||
"post_capture": {
|
||||
"label": "Секунды после захвата",
|
||||
"description": "Количество секунд после события обнаружения, которое необходимо включить в запись."
|
||||
},
|
||||
"retain": {
|
||||
"label": "Сохранение событий",
|
||||
"description": "Настройки хранения записей событий обнаружения.",
|
||||
"days": {
|
||||
"label": "Дни хранения",
|
||||
"description": "Количество дней для хранения записей событий обнаружения."
|
||||
},
|
||||
"mode": {
|
||||
"label": "Режим хранения",
|
||||
"description": "Режим сохранения: all (сохранить все сегменты), motion (сохранить сегменты с движением) или active_objects (сохранить сегменты с активными объектами)."
|
||||
}
|
||||
}
|
||||
},
|
||||
"export": {
|
||||
"label": "Экспортировать конфигурацию",
|
||||
"description": "Настройки, используемые при экспорте записей, такие как замедленная съемка и аппаратное ускорение.",
|
||||
"hwaccel_args": {
|
||||
"label": "Экспортировать аргументы hwaccel",
|
||||
"description": "Аргументы аппаратного ускорения, используемые для операций экспорта/перекодирования."
|
||||
},
|
||||
"max_concurrent": {
|
||||
"label": "Максимальный одновременный экспорт",
|
||||
"description": "Максимальное количество заданий экспорта, обрабатываемых одновременно."
|
||||
},
|
||||
"chapters": {
|
||||
"label": "Метаданные глав для встраивания в экспортированные записи"
|
||||
}
|
||||
},
|
||||
"preview": {
|
||||
"label": "Предварительный просмотр конфигурации",
|
||||
"description": "Настройки, управляющие качеством предварительного просмотра записи, отображаемого в пользовательском интерфейсе.",
|
||||
"quality": {
|
||||
"label": "Предварительный просмотр качества",
|
||||
"description": "Уровень качества предпросмотра (very_low, low, medium, high, very_high)."
|
||||
}
|
||||
},
|
||||
"enabled_in_config": {
|
||||
"label": "Исходное состояние записи",
|
||||
"description": "Указывает, была ли запись включена в исходной статической конфигурации."
|
||||
}
|
||||
},
|
||||
"review": {
|
||||
"label": "Проверка",
|
||||
"alerts": {
|
||||
"label": "Конфигурация оповещений",
|
||||
"description": "Настройки, для которых отслеживаемые объекты генерируют оповещения и как оповещения сохраняются.",
|
||||
"enabled": {
|
||||
"label": "Включить оповещения"
|
||||
},
|
||||
"labels": {
|
||||
"label": "Предупреждающие ярлыки",
|
||||
"description": "Список меток объектов, которые квалифицируются как оповещения (например: машина, человек)."
|
||||
},
|
||||
"required_zones": {
|
||||
"label": "Обязательные зоны",
|
||||
"description": "Зоны, в которые должен войти объект, чтобы считаться оповещением; оставьте пустым, чтобы разрешить любую зону."
|
||||
},
|
||||
"enabled_in_config": {
|
||||
"label": "Исходное состояние оповещений",
|
||||
"description": "Отслеживает, были ли оповещения изначально включены в статической конфигурации."
|
||||
},
|
||||
"cutoff_time": {
|
||||
"label": "Время окончания оповещений",
|
||||
"description": "Секунды ожидания после отсутствия активности, вызывающей оповещение, прежде чем отключать оповещение."
|
||||
}
|
||||
},
|
||||
"detections": {
|
||||
"label": "Конфигурация обнаружения",
|
||||
"description": "Настройки, для которых отслеживаемые объекты генерируют обнаружения (без оповещения) и порядок сохранения обнаружений.",
|
||||
"enabled": {
|
||||
"label": "Включить обнаружение"
|
||||
},
|
||||
"labels": {
|
||||
"label": "Метки обнаружения",
|
||||
"description": "\"Список меток объектов, которые квалифицируются как события обнаружения."
|
||||
},
|
||||
"required_zones": {
|
||||
"label": "Обязательные зоны",
|
||||
"description": "Зоны, в которые должен войти объект, чтобы считаться обнаруженным; оставьте пустым, чтобы разрешить любую зону."
|
||||
},
|
||||
"cutoff_time": {
|
||||
"label": "Время прекращения обнаружения",
|
||||
"description": "Секунды ожидания после отсутствия активности, вызывающей обнаружение, прежде чем отключать обнаружение."
|
||||
},
|
||||
"enabled_in_config": {
|
||||
"label": "Исходное состояние обнаружений",
|
||||
"description": "Отслеживает, были ли изначально включены обнаружения в статической конфигурации."
|
||||
}
|
||||
},
|
||||
"genai": {
|
||||
"label": "Конфигурация GenAI",
|
||||
"description": "Контролирует использование генеративного искусственного интеллекта для создания описаний и резюме элементов обзора.",
|
||||
"enabled": {
|
||||
"label": "Включить описания GenAI",
|
||||
"description": "Включить или отключить описания и сводки GenAI для элементов проверки."
|
||||
},
|
||||
"alerts": {
|
||||
"label": "Включить GenAI для оповещений",
|
||||
"description": "Используйте GenAI для создания описаний элементов оповещений."
|
||||
},
|
||||
"detections": {
|
||||
"label": "Включить GenAI для детекций",
|
||||
"description": "Используйте GenAI для создания описаний элементов обнаружения."
|
||||
},
|
||||
"image_source": {
|
||||
"label": "Проверьте источник изображения",
|
||||
"description": "\"Источник изображений, отправляемых на GenAI ('preview' или 'recordings'); 'recordings' использует кадры более высокого качества, но больше токенов."
|
||||
},
|
||||
"additional_concerns": {
|
||||
"label": "Дополнительные проблемы",
|
||||
"description": "Список дополнительных проблем или примечаний, которые GenAI следует учитывать при оценке активности на этой камере."
|
||||
},
|
||||
"debug_save_thumbnails": {
|
||||
"label": "Сохранить миниатюры",
|
||||
"description": "Сохраняйте миниатюры, которые отправляются поставщику GenAI для отладки и проверки."
|
||||
},
|
||||
"enabled_in_config": {
|
||||
"label": "Исходное состояние GenAI",
|
||||
"description": "Отслеживает, была ли изначально включена проверка GenAI в статической конфигурации."
|
||||
},
|
||||
"preferred_language": {
|
||||
"label": "Предпочитаемый язык",
|
||||
"description": "Предпочитаемый язык для запроса у провайдера GenAI сгенерированных ответов."
|
||||
},
|
||||
"activity_context_prompt": {
|
||||
"label": "Промпт контекста активности",
|
||||
"description": "Пользовательский промпт, описывающий, какая активность считается подозрительной, а какая нет, для контекста сводок GenAI."
|
||||
}
|
||||
}
|
||||
},
|
||||
"snapshots": {
|
||||
"label": "Снимки",
|
||||
"enabled": {
|
||||
"label": "Включить снимки"
|
||||
},
|
||||
"timestamp": {
|
||||
"label": "Наложение временной метки",
|
||||
"description": "Наложение временной метки на снимки из API."
|
||||
},
|
||||
"bounding_box": {
|
||||
"label": "Наложение ограничивающей рамки",
|
||||
"description": "Рисовать ограничивающие рамки отслеживаемых объектов на снимках из API."
|
||||
},
|
||||
"crop": {
|
||||
"label": "Обрезать снимок",
|
||||
"description": "Обрезать снимки API по ограничивающей рамке обнаруженного объекта."
|
||||
},
|
||||
"required_zones": {
|
||||
"label": "Обязательные зоны",
|
||||
"description": "Зоны, в которые должен войти объект, чтобы снимок был сохранен."
|
||||
},
|
||||
"height": {
|
||||
"label": "Высота снимка",
|
||||
"description": "Высота (в пикселях) для изменения размера снимков с API на; оставьте пустым, чтобы сохранить исходный размер."
|
||||
},
|
||||
"retain": {
|
||||
"label": "Хранение снимков",
|
||||
"description": "Настройки хранения для снимков, включая дни по умолчанию и переопределения для каждого объекта.",
|
||||
"default": {
|
||||
"label": "Срок хранения по умолчанию",
|
||||
"description": "Число дней по умолчанию для хранения снимков."
|
||||
},
|
||||
"objects": {
|
||||
"label": "Сохранение объекта",
|
||||
"description": "Переопределения дней хранения снимков для каждого объекта."
|
||||
}
|
||||
},
|
||||
"quality": {
|
||||
"label": "Качество снимка",
|
||||
"description": "Качество кодирования для сохраненных снимков (0–100)."
|
||||
}
|
||||
},
|
||||
"timestamp_style": {
|
||||
"label": "Стиль временной метки",
|
||||
"position": {
|
||||
"label": "Позиция временной метки",
|
||||
"description": "Положение отметки времени на изображении (tl/tr/bl/br)."
|
||||
},
|
||||
"format": {
|
||||
"label": "Формат временной метки",
|
||||
"description": "Строка формата даты и времени, используемая для меток времени (коды формата даты и времени Python)."
|
||||
},
|
||||
"color": {
|
||||
"label": "Цвет временной метки",
|
||||
"description": "Значения цвета RGB для текста метки времени (все значения от 0 до 255).",
|
||||
"red": {
|
||||
"label": "Красный",
|
||||
"description": "Красный компонент (0–255) для цвета метки времени."
|
||||
},
|
||||
"green": {
|
||||
"label": "Зелёный",
|
||||
"description": "Зеленый компонент (0–255) для цвета метки времени."
|
||||
},
|
||||
"blue": {
|
||||
"label": "Синий",
|
||||
"description": "Синий компонент (0–255) для цвета метки времени."
|
||||
}
|
||||
},
|
||||
"thickness": {
|
||||
"label": "Толщина временной метки",
|
||||
"description": "Толщина линии текста метки времени."
|
||||
},
|
||||
"effect": {
|
||||
"label": "Эффект временной метки",
|
||||
"description": "Визуальный эффект для текста метки времени (нет, сплошной, тень)."
|
||||
}
|
||||
},
|
||||
"semantic_search": {
|
||||
"label": "Семантический поиск",
|
||||
"triggers": {
|
||||
"label": "Триггеры",
|
||||
"description": "Действия и критерии соответствия для триггеров семантического поиска, специфичных для камеры.",
|
||||
"friendly_name": {
|
||||
"label": "Дружественное имя",
|
||||
"description": "Необязательное понятное имя, отображаемое в пользовательском интерфейсе для этого триггера."
|
||||
},
|
||||
"enabled": {
|
||||
"label": "Включить этот триггер",
|
||||
"description": "Включить или отключить этот триггер семантического поиска."
|
||||
},
|
||||
"type": {
|
||||
"label": "Тип триггера",
|
||||
"description": "Тип триггера: 'thumbnail' (совпадение с изображением) или 'description' (совпадение с текстом)."
|
||||
},
|
||||
"data": {
|
||||
"label": "Содержимое триггера",
|
||||
"description": "Текстовая фраза или идентификатор миниатюры для сопоставления с отслеживаемыми объектами."
|
||||
},
|
||||
"threshold": {
|
||||
"label": "Порог срабатывания",
|
||||
"description": "Минимальный показатель сходства (0–1), необходимый для активации этого триггера."
|
||||
},
|
||||
"actions": {
|
||||
"label": "Триггерные действия",
|
||||
"description": "Список действий при срабатывании триггера (notification, sub_label, attribute)."
|
||||
}
|
||||
}
|
||||
},
|
||||
"face_recognition": {
|
||||
"label": "Распознавание лиц",
|
||||
"enabled": {
|
||||
"label": "Включить распознавание лиц"
|
||||
},
|
||||
"min_area": {
|
||||
"label": "Минимальная площадь лица",
|
||||
"description": "Минимальная площадь (пиксели) обнаруженного лицевого окна, необходимая для попытки распознавания."
|
||||
}
|
||||
},
|
||||
"lpr": {
|
||||
"label": "Распознавание номерных знаков",
|
||||
"description": "Настройки распознавания номерных знаков, включая пороговые значения обнаружения, форматирование и известные номера.",
|
||||
"enabled": {
|
||||
"label": "Включить LPR"
|
||||
},
|
||||
"min_area": {
|
||||
"label": "Минимальная площадь номера",
|
||||
"description": "Минимальная площадь номерного знака (в пикселях), необходимая для попытки распознавания."
|
||||
},
|
||||
"enhancement": {
|
||||
"label": "Уровень улучшения",
|
||||
"description": "Уровень улучшения (0–10), применяемый к кропам номеров перед OCR; более высокие значения не всегда улучшают результат, уровни выше 5 могут работать только с ночными номерами — используйте осторожно."
|
||||
},
|
||||
"expire_time": {
|
||||
"label": "Время истечения (сек)",
|
||||
"description": "Время в секундах, после которого невидимый номер удаляется из трекера (только для выделенных LPR-камер)."
|
||||
}
|
||||
},
|
||||
"profiles": {
|
||||
"label": "Профили"
|
||||
},
|
||||
"onvif": {
|
||||
"label": "ONVIF",
|
||||
"description": "Соединение ONVIF и настройки автослежения PTZ для этой камеры.",
|
||||
"host": {
|
||||
"label": "Хост ONVIF",
|
||||
"description": "Хост (и дополнительная схема) для службы ONVIF для этой камеры."
|
||||
},
|
||||
"port": {
|
||||
"label": "Порт ONVIF",
|
||||
"description": "Номер порта для службы ONVIF."
|
||||
},
|
||||
"user": {
|
||||
"label": "Имя пользователя ONVIF",
|
||||
"description": "Имя пользователя для аутентификации ONVIF; на некоторых устройствах требуется администратор для ONVIF."
|
||||
},
|
||||
"password": {
|
||||
"label": "Пароль ONVIF",
|
||||
"description": "Пароль для аутентификации ONVIF."
|
||||
},
|
||||
"tls_insecure": {
|
||||
"label": "Отключить проверку TLS",
|
||||
"description": "Пропустить проверку TLS и отключить дайджест-аутентификацию для ONVIF (небезопасно; использовать только в безопасных сетях)."
|
||||
},
|
||||
"profile": {
|
||||
"label": "Профиль ONVIF",
|
||||
"description": "Конкретный медиа-профиль ONVIF, используемый для элемента управления PTZ, соответствующий токену или имени. Если этот параметр не установлен, автоматически выбирается первый профиль с действительной конфигурацией PTZ."
|
||||
},
|
||||
"autotracking": {
|
||||
"label": "Автотрекинг",
|
||||
"description": "Автоматически отслеживайте движущиеся объекты и удерживайте их в центре кадра с помощью движений камеры PTZ.",
|
||||
"enabled": {
|
||||
"label": "Включить автотрекинг",
|
||||
"description": "Включить или отключить автоматическое PTZ-сопровождение обнаруженных объектов."
|
||||
},
|
||||
"calibrate_on_startup": {
|
||||
"label": "Калибровка при запуске",
|
||||
"description": "Измерьте скорость двигателя PTZ при запуске, чтобы повысить точность отслеживания. Frigate обновит конфигурацию с помощью motion_weights после калибровки."
|
||||
},
|
||||
"zooming": {
|
||||
"label": "Режим масштабирования",
|
||||
"description": "Управляет масштабированием: отключенный (только панорамирование и наклон), полный (наиболее совместимый режим) или условный (одновременное панорамирование, наклон и масштабирование)."
|
||||
},
|
||||
"zoom_factor": {
|
||||
"label": "Уровень масштабирования",
|
||||
"description": "Уровень зума на отслеживаемых объектах. Меньшие значения оставляют в кадре больше сцены; большие приближают сильнее, но могут потерять объект. Значения от 0.1 до 0.75."
|
||||
},
|
||||
"track": {
|
||||
"label": "Отслеживаемые объекты",
|
||||
"description": "Список типов объектов, которые должны запускать автоматическое отслеживание."
|
||||
},
|
||||
"required_zones": {
|
||||
"label": "Обязательные зоны",
|
||||
"description": "Прежде чем начнется автоматическое слежение, объекты должны войти в одну из этих зон."
|
||||
},
|
||||
"return_preset": {
|
||||
"label": "Вернуть предустановку",
|
||||
"description": "Имя предустановки ONVIF, настроенное в прошивке камеры, к которому можно вернуться после завершения отслеживания."
|
||||
},
|
||||
"timeout": {
|
||||
"label": "Тайм-аут возврата",
|
||||
"description": "Подождите это количество секунд после потери отслеживания, прежде чем возвращать камеру в заданное положение."
|
||||
},
|
||||
"movement_weights": {
|
||||
"label": "Вес движения",
|
||||
"description": "Калибровочные значения автоматически генерируются при калибровке камеры. Не вносите изменения вручную."
|
||||
},
|
||||
"enabled_in_config": {
|
||||
"label": "Исходное состояние автотрека",
|
||||
"description": "Внутреннее поле для отслеживания того, включено ли в конфигурации автоотслеживание."
|
||||
}
|
||||
},
|
||||
"ignore_time_mismatch": {
|
||||
"label": "Игнорировать несоответствие времени",
|
||||
"description": "Игнорировать расхождение времени между камерой и сервером Frigate при обмене по ONVIF."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -31,7 +31,7 @@
|
||||
"classification": "Распознавание",
|
||||
"enrichments": "Обогащения",
|
||||
"triggers": "Триггеры",
|
||||
"cameraManagement": "Управление",
|
||||
"cameraManagement": "Управление камерой",
|
||||
"cameraReview": "Обзор",
|
||||
"roles": "Роли",
|
||||
"general": "Общее",
|
||||
@@ -70,7 +70,28 @@
|
||||
"cameraConfigReview": "Обзор событий",
|
||||
"cameraAudioEvents": "Обнаружение аудио",
|
||||
"cameraAudioTranscription": "Расшифровка аудиозаписи",
|
||||
"cameraNotifications": "Уведомления"
|
||||
"cameraNotifications": "Уведомления",
|
||||
"globalRecording": "Запись",
|
||||
"globalSnapshots": "Снимки",
|
||||
"globalFfmpeg": "FFmpeg",
|
||||
"globalMotion": "Обнаружение движения",
|
||||
"globalObjects": "Объекты",
|
||||
"globalLivePlayback": "Воспроизведение в реальном времени",
|
||||
"globalTimestampStyle": "Стиль временной метки",
|
||||
"systemDatabase": "База данных",
|
||||
"systemTls": "TLS",
|
||||
"cameraLivePlayback": "Воспроизведение в реальном времени",
|
||||
"cameraBirdseye": "Birdseye",
|
||||
"cameraFaceRecognition": "Распознавание лиц",
|
||||
"cameraLpr": "Распознавание номерных знаков",
|
||||
"cameraMqttConfig": "MQTT",
|
||||
"cameraOnvif": "ONVIF",
|
||||
"cameraUi": "Интерфейс камеры",
|
||||
"cameraTimestampStyle": "Стиль временной метки",
|
||||
"cameraMqtt": "MQTT камеры",
|
||||
"maintenance": "Обслуживание",
|
||||
"mediaSync": "Синхронизация медиа",
|
||||
"regionGrid": "Сетка регионов"
|
||||
},
|
||||
"dialog": {
|
||||
"unsavedChanges": {
|
||||
@@ -260,7 +281,7 @@
|
||||
"hide": "Скрыть пароль",
|
||||
"requirements": {
|
||||
"title": "Требования к паролю:",
|
||||
"length": "Не менее 8 символов",
|
||||
"length": "Не менее 12 символов",
|
||||
"uppercase": "Как минимум одна заглавная буква",
|
||||
"digit": "Как минимум одна цифра",
|
||||
"special": "Хотя бы один специальный символ (!@#$%^&*(),.?\":{}|<>)"
|
||||
@@ -328,7 +349,8 @@
|
||||
"notificationUnavailable": {
|
||||
"documentation": "Читать документацию",
|
||||
"title": "Уведомления недоступны",
|
||||
"desc": "Веб-уведомления требуют защищённого контекста (<code>https://…</code>). Это ограничение браузера. Получите безопасный доступ к Frigate, чтобы использовать уведомления."
|
||||
"desc": "Веб-уведомления требуют защищённого контекста (<code>https://…</code>). Это ограничение браузера. Получите безопасный доступ к Frigate, чтобы использовать уведомления.",
|
||||
"descPwa": "На iOS веб-уведомления доступны только в том случае, если Frigate установлен на главном экране. Откройте меню <strong>Поделиться</strong>, выберите <strong>Добавить на главный экран</strong>, затем откройте Frigate с помощью нового значка, чтобы зарегистрировать это устройство для уведомлений."
|
||||
},
|
||||
"email": {
|
||||
"title": "Email",
|
||||
@@ -498,6 +520,10 @@
|
||||
},
|
||||
"toast": {
|
||||
"success": "Зона ({{zoneName}}) сохранена."
|
||||
},
|
||||
"enabled": {
|
||||
"title": "Включено",
|
||||
"description": "Активна ли эта зона и включена ли она в файле конфигурации. Если отключено, его нельзя включить с помощью MQTT. Отключенные зоны игнорируются во время выполнения."
|
||||
}
|
||||
},
|
||||
"motionMasks": {
|
||||
@@ -527,6 +553,12 @@
|
||||
"title": "{{polygonName}} сохранена.",
|
||||
"noName": "Маска движения сохранена."
|
||||
}
|
||||
},
|
||||
"defaultName": "Маска движения {{number}}",
|
||||
"name": {
|
||||
"title": "Имя",
|
||||
"description": "Необязательное отображаемое имя для этой маски движения.",
|
||||
"placeholder": "Введите имя…"
|
||||
}
|
||||
},
|
||||
"filter": {
|
||||
@@ -575,12 +607,32 @@
|
||||
},
|
||||
"reset": {
|
||||
"label": "Удалить все точки"
|
||||
},
|
||||
"type": {
|
||||
"zone": "зона",
|
||||
"motion_mask": "маска движения",
|
||||
"object_mask": "маска объекта"
|
||||
},
|
||||
"revertOverride": {
|
||||
"title": "Вернуться к базовой конфигурации",
|
||||
"desc": "Это приведет к удалению переопределения профиля для {{type}} <em>{{name}}</em> и возврату к базовой конфигурации."
|
||||
}
|
||||
},
|
||||
"speed": {
|
||||
"error": {
|
||||
"mustBeGreaterOrEqualTo": "Порог скорости должен быть не меньше 0,1."
|
||||
}
|
||||
},
|
||||
"id": {
|
||||
"error": {
|
||||
"mustNotBeEmpty": "Идентификатор не должен быть пустым.",
|
||||
"alreadyExists": "Маска с таким идентификатором уже существует для этой камеры."
|
||||
}
|
||||
},
|
||||
"name": {
|
||||
"error": {
|
||||
"mustNotBeEmpty": "Имя не должно быть пустым."
|
||||
}
|
||||
}
|
||||
},
|
||||
"toast": {
|
||||
@@ -615,11 +667,26 @@
|
||||
"title": "{{polygonName}} сохранена.",
|
||||
"noName": "Маска объектов сохранена."
|
||||
}
|
||||
},
|
||||
"name": {
|
||||
"title": "Имя",
|
||||
"description": "Необязательное отображаемое имя для этой маски объекта.",
|
||||
"placeholder": "Введите имя…"
|
||||
}
|
||||
},
|
||||
"restart_required": "Требуется перезапуск (изменены маски/зоны)",
|
||||
"objectMaskLabel": "Маска объекта {{number}} {{label}}",
|
||||
"motionMaskLabel": "Маска движения {{number}}"
|
||||
"objectMaskLabel": "Маска объекта {{number}}",
|
||||
"motionMaskLabel": "Маска движения {{number}}",
|
||||
"disabledInConfig": "Элемент отключен в файле конфигурации",
|
||||
"addDisabledProfile": "Сначала добавьте в базовую конфигурацию, затем переопределите в профиле",
|
||||
"profileBase": "(база)",
|
||||
"profileOverride": "(переопределить)",
|
||||
"masks": {
|
||||
"enabled": {
|
||||
"title": "Включено",
|
||||
"description": "Включена ли эта маска в файле конфигурации. Если отключено, его нельзя включить с помощью MQTT. Отключенные маски игнорируются во время выполнения."
|
||||
}
|
||||
}
|
||||
},
|
||||
"motionDetectionTuner": {
|
||||
"desc": {
|
||||
@@ -708,13 +775,13 @@
|
||||
"snapshotConfig": {
|
||||
"documentation": "Читать документацию",
|
||||
"title": "Настройки снимков",
|
||||
"cleanCopyWarning": "У некоторых камер включены снимки (snapshots), но отключена опция чистой копии (clean copy). Чтобы иметь возможность отправлять изображения с этих камер в Frigate+, необходимо включить параметр <code>clean_copy</code> в конфигурации снимков.",
|
||||
"cleanCopyWarning": "У некоторых камер снимки отключены",
|
||||
"table": {
|
||||
"cleanCopySnapshots": "Снимки <code>clean_copy</code>",
|
||||
"camera": "Камера",
|
||||
"snapshots": "Снимки"
|
||||
},
|
||||
"desc": "Отправка в Frigate+ требует, чтобы в вашей конфигурации были включены как снимки (snapshots), так и снимки <code>clean_copy</code>."
|
||||
"desc": "Отправка в Frigate+ требует, чтобы в вашей конфигурации были включены снимки."
|
||||
},
|
||||
"title": "Настройки Frigate+",
|
||||
"apiKey": {
|
||||
@@ -729,7 +796,7 @@
|
||||
"modelType": "Тип модели",
|
||||
"trainDate": "Дата обучения",
|
||||
"error": "Не удалось загрузить информацию о модели",
|
||||
"availableModels": "Доступные модели",
|
||||
"availableModels": "Доступные Frigate+ модели",
|
||||
"loadingAvailableModels": "Загрузка доступных моделей…",
|
||||
"modelSelect": "Здесь можно выбрать ваши доступные модели на Frigate+. Обратите внимание, что могут быть выбраны только модели, совместимые с текущей конфигурацией детектора.",
|
||||
"baseModel": "Базовая модель",
|
||||
@@ -740,6 +807,14 @@
|
||||
"plusModelType": {
|
||||
"baseModel": "Базовая модель",
|
||||
"userModel": "Дообученная"
|
||||
},
|
||||
"noModelLoaded": "Ни одна модель Frigate+ в настоящее время не загружена.",
|
||||
"selectModel": "Выберите модель",
|
||||
"noModelsAvailable": "Нет доступных моделей",
|
||||
"filter": {
|
||||
"ariaLabel": "Фильтровать модели по типу",
|
||||
"baseModels": "Базовые модели",
|
||||
"fineTunedModels": "Настроенные модели"
|
||||
}
|
||||
},
|
||||
"toast": {
|
||||
@@ -747,7 +822,15 @@
|
||||
"error": "Не удалось сохранить изменения конфигурации: {{errorMessage}}"
|
||||
},
|
||||
"restart_required": "Требуется перезапуск (изменена модель Frigate+)",
|
||||
"unsavedChanges": "Настройки Frigate+ не сохранены"
|
||||
"unsavedChanges": "Настройки Frigate+ не сохранены",
|
||||
"description": "Frigate+ — это служба подписки, которая предоставляет доступ к дополнительным функциям и возможностям вашего экземпляра Frigate, включая возможность использовать пользовательские модели обнаружения объектов, обученные на ваших собственных данных. Здесь вы можете управлять настройками модели Frigate+.",
|
||||
"cardTitles": {
|
||||
"api": "API",
|
||||
"currentModel": "Текущая модель",
|
||||
"otherModels": "Другие модели",
|
||||
"configuration": "Конфигурация"
|
||||
},
|
||||
"changeInDetectorsAndModel": "Изменить модель"
|
||||
},
|
||||
"enrichments": {
|
||||
"title": "Настройки обогащения",
|
||||
@@ -994,7 +1077,7 @@
|
||||
"brands": {
|
||||
"reolink-rtsp": "RTSP от Reolink не рекомендуется. Включите HTTP в настройках камеры и перезапустите мастер настройки камеры."
|
||||
},
|
||||
"customUrlRtspRequired": "Пользовательские URL должны начинаться с \"rtsp://\". Для потоков камер, не использующих RTSP, требуется ручная настройка."
|
||||
"customUrlRtspRequired": "Пользовательские URL должны начинаться с \"rtsp://\" или \"rtsps://\". Для потоков камер, не использующих RTSP, требуется ручная настройка."
|
||||
},
|
||||
"docs": {
|
||||
"reolink": "https://docs.frigate.video/configuration/camera_specific.html#reolink-cameras"
|
||||
@@ -1158,7 +1241,8 @@
|
||||
},
|
||||
"hikvision": {
|
||||
"substreamWarning": "Подпоток 1 заблокирован на низком разрешении. Многие камеры Hikvision поддерживают дополнительные подпотоки, которые необходимо включить в настройках камеры. Рекомендуется проверить и использовать эти потоки, если они доступны."
|
||||
}
|
||||
},
|
||||
"resolutionUnknown": "Разрешение этого потока проверить не удалось. Вам следует вручную установить разрешение обнаружения в настройках или вашей конфигурации."
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -1231,11 +1315,13 @@
|
||||
"selectCamera": "Выбрать камеру",
|
||||
"backToSettings": "Вернуться к настройкам камеры",
|
||||
"streams": {
|
||||
"title": "Включить / Отключить камеры",
|
||||
"title": "Состояние и характеристики камеры",
|
||||
"desc": "Временно отключить камеру до перезапуска Frigate. Отключение камеры полностью останавливает обработку потоков этой камеры в Frigate. Обнаружение, запись и отладка будут недоступны.<br /> <em>Примечание: Это не отключает рестриминг go2rtc.</em>",
|
||||
"label": "Состояние камеры",
|
||||
"status": {
|
||||
"disabled": "Отключено"
|
||||
"disabled": "Отключено",
|
||||
"on": "Вкл",
|
||||
"off": "Выкл"
|
||||
},
|
||||
"enableSuccess": "Включена {{cameraName}}. Перезапустите Frigate для применения изменений.",
|
||||
"disableSuccess": "Отключено {{cameraName}} и сохранено в конфигурации.",
|
||||
@@ -1252,8 +1338,12 @@
|
||||
"webuiUrlHelp": "URL-адрес для посещения веб-UI камеры непосредственно с отладочного меню. Оставьте пустым, чтобы отключить ссылку.",
|
||||
"webuiUrlInvalid": "Должен быть действительный URL (например, https://example.com).",
|
||||
"dashboardLabel": "Показывать на Live dashboard",
|
||||
"dashboardHelp": "Показывать эту камеру на Live dashboard."
|
||||
}
|
||||
"dashboardHelp": "Отобразить эту камеру на панели мониторинга «Все камеры» в режиме реального времени по умолчанию. Она остается доступной везде, включая группы камер.",
|
||||
"reviewLabel": "Показать в обзоре",
|
||||
"reviewHelp": "Покажите эту камеру в обзоре, включая фильтр камеры, обзор движения и просмотр истории."
|
||||
},
|
||||
"description": "Задайте рабочее состояние для каждой камеры.<br /><br /><strong>Вкл</strong>: потоки обрабатываются в обычном режиме.<br /><strong>Выкл</strong>: временно приостанавливает обработку. Не сохраняется между перезапусками Frigate.<br /><strong>Отключено</strong>: останавливает обработку и сохраняет изменение в конфигурации. Для повторного включения отключённой камеры требуется перезапуск.<br /><br /><em>Примечание: отключение не влияет на рестримы go2rtc.</em><br /><br />Перетащите маркер, чтобы изменить порядок активных камер в интерфейсе, включая панель мониторинга Live и списки выбора камер.",
|
||||
"disabledSubheading": "Отключено в конфигурации"
|
||||
},
|
||||
"cameraConfig": {
|
||||
"add": "Добавить камеру",
|
||||
@@ -1287,7 +1377,119 @@
|
||||
"deleteCameraDialog": {
|
||||
"confirmWarning": "Удаление <strong>{{cameraName}}</strong> не может быть отменено.",
|
||||
"success": "Камера {{cameraName}} успешно удалена",
|
||||
"error": "Не удалось удалить камеру {{cameraName}}"
|
||||
"error": "Не удалось удалить камеру {{cameraName}}",
|
||||
"description": "Удаление камеры приведет к безвозвратному удалению всех записей, отслеживаемых объектов и конфигурации этой камеры. Любые потоки go2rtc, связанные с этой камерой, возможно, придется удалить вручную.",
|
||||
"selectPlaceholder": "Выберите камеру…",
|
||||
"confirmTitle": "Вы уверены?",
|
||||
"deleteExports": "Также удалите экспорт для этой камеры",
|
||||
"confirmButton": "Удалить навсегда",
|
||||
"title": "Удалить камеру"
|
||||
},
|
||||
"deleteCamera": "Удалить камеру",
|
||||
"description": "Добавляйте, редактируйте и удаляйте камеры, управляйте состоянием каждой камеры и настраивайте переопределения для профилей и типа камеры. Чтобы настроить потоки, обнаружение, движение и другие параметры камеры, выберите нужный раздел в конфигурации камеры.",
|
||||
"profiles": {
|
||||
"title": "Переопределения профиля камеры",
|
||||
"selectLabel": "Выберите профиль",
|
||||
"description": "Настройте, какие камеры будут включаться или выключаться при активации профиля. Камеры, для которых установлено значение «Наследовать», сохраняют состояние по умолчанию.",
|
||||
"inherit": "Наследовать",
|
||||
"on": "Вкл",
|
||||
"off": "Выкл"
|
||||
},
|
||||
"cameraType": {
|
||||
"title": "Тип камеры",
|
||||
"label": "Тип камеры",
|
||||
"description": "Установите тип для каждой камеры. Специальные камеры LPR — это одноцелевые камеры с мощным оптическим зумом для захвата номерных знаков удаленных транспортных средств. Большинство камер должны использовать камеры обычного типа, если только камера не предназначена специально для LPR и не обеспечивает четкое изображение номерных знаков.",
|
||||
"normal": "Нормальный",
|
||||
"dedicatedLpr": "Выделенный LPR",
|
||||
"saveSuccess": "Обновлен тип камеры для {{cameraName}}. Перезапустите Frigate, чтобы применить изменения."
|
||||
},
|
||||
"clone": {
|
||||
"sectionTitle": "Настройки клонирования",
|
||||
"sectionDescription": "Скопируйте конфигурацию с одной камеры на другую камеру или на новую.",
|
||||
"button": "Настройки клонирования",
|
||||
"title": "Клонировать настройки камеры",
|
||||
"description": "Скопируйте конфигурацию камеры на одну или несколько других камер либо на новую камеру. Идентичность (имя, отображаемое имя, URL веб-интерфейса, порядок отображения) никогда не копируется.",
|
||||
"source": {
|
||||
"label": "Исходная камера",
|
||||
"placeholder": "Выберите исходную камеру",
|
||||
"required": "Выберите исходную камеру"
|
||||
},
|
||||
"target": {
|
||||
"legend": "Цель",
|
||||
"newRadio": "Новая камера",
|
||||
"newNameLabel": "Имя камеры",
|
||||
"newNamePlaceholder": "например, back_door или Back Door",
|
||||
"newNameRequired": "Название камеры обязательно.",
|
||||
"newNameInvalid": "Неверное имя камеры",
|
||||
"newNameCollision": "Камера с таким названием уже существует",
|
||||
"newStreamsForced": "Потоки всегда копируются для новой камеры.",
|
||||
"existingCamerasRadio": "Существующие камеры",
|
||||
"allCameras": "Все камеры",
|
||||
"existingPlaceholder": "Выберите хотя бы одну камеру",
|
||||
"existingDisabled": "Нет других камер для копирования"
|
||||
},
|
||||
"categories": {
|
||||
"legend": "Настройки для клонирования",
|
||||
"description": "Выберите, какие настройки копировать с исходной камеры.",
|
||||
"selectAll": "Выбрать всё",
|
||||
"selectNone": "Ничего не выбрать",
|
||||
"resetDefaults": "Сбросить настройки по умолчанию",
|
||||
"general": "Общие",
|
||||
"spatial": "Пространственные настройки",
|
||||
"streams": "Потоки",
|
||||
"spatialWarningTitle": "Несоответствие разрешения",
|
||||
"spatialWarning": "Разрешение обнаружения исходной камеры {{srcCamera}} ({{srcWidth}}×{{srcHeight}}) отличается от: {{cameras}}. На этих камерах полигоны могут не совпадать. Эти значения по умолчанию отключены; включить копирование как есть.",
|
||||
"restartHint": "Требуется перезагрузка",
|
||||
"items": {
|
||||
"record": "Запись",
|
||||
"snapshots": "Снимки",
|
||||
"review": "Обзор событий",
|
||||
"motion": "Обнаружение движения",
|
||||
"objects": "Объекты",
|
||||
"audio": "Обнаружение аудио",
|
||||
"audio_transcription": "Расшифровка аудио",
|
||||
"notifications": "Уведомления",
|
||||
"birdseye": "Birdseye",
|
||||
"mqtt": "MQTT",
|
||||
"timestamp_style": "Стиль временной метки",
|
||||
"onvif": "ONVIF",
|
||||
"lpr": "Распознавание номерных знаков",
|
||||
"face_recognition": "Распознавание лиц",
|
||||
"semantic_search": "Семантический поиск",
|
||||
"genai": "Генеративный ИИ",
|
||||
"type": "Тип камеры (обычная / выделенная LPR)",
|
||||
"profiles": "Профили",
|
||||
"detect": "Определить размеры",
|
||||
"zones": "Зоны",
|
||||
"motion_mask": "Маски движения",
|
||||
"object_masks": "Маски объектов",
|
||||
"ffmpeg_live": "Стрим URLs и роли"
|
||||
}
|
||||
},
|
||||
"footer": {
|
||||
"changeCount_one": "Изменение {{count}} будет применено",
|
||||
"changeCount_few": "Изменение {{count}} будет применено",
|
||||
"changeCount_many": "Изменение {{count}} будет применено",
|
||||
"restartNeeded": "Для некоторых изменений потребуется перезагрузка.",
|
||||
"liveOnly": "Все изменения вступят в силу без перезагрузки.",
|
||||
"submit": "Клонировать",
|
||||
"submitting": "Клонирование…"
|
||||
},
|
||||
"toast": {
|
||||
"success": "Настройки скопированы в {{cameraName}}.",
|
||||
"successWithRestart": "Настройки скопированы в {{cameraName}}. Перезапустите Frigate, чтобы применить все изменения.",
|
||||
"successMulti_one": "Настройки скопированы в {{count}} камеру",
|
||||
"successMulti_few": "Настройки скопированы в {{count}} камеры",
|
||||
"successMulti_many": "Настройки скопированы в {{count}} камер",
|
||||
"successMultiWithRestart_one": "Настройки скопированы в {{count}} камеру. Перезапустите Frigate, чтобы применить все изменения.",
|
||||
"successMultiWithRestart_few": "Настройки скопированы в {{count}} камеры. Перезапустите Frigate, чтобы применить все изменения.",
|
||||
"successMultiWithRestart_many": "Настройки скопированы в {{count}} камер. Перезапустите Frigate, чтобы применить все изменения.",
|
||||
"partialFailure": "Применены {{successCount}} разделов; Ошибка «{{failedSection}}»: {{errorMessage}}",
|
||||
"partialFailureMulti": "Скопировано на {{successCount}}камеру(ы); не удалось для {{failed}}: {{errorMessage}}",
|
||||
"newCameraPartialFailure": "Камера {{cameraName}} создана, но некоторые настройки не удалось скопировать: {{errorMessage}}",
|
||||
"sourceMissing": "Исходная камера больше не существует",
|
||||
"submitError": "Не удалось клонировать камеру: {{errorMessage}}"
|
||||
}
|
||||
}
|
||||
},
|
||||
"cameraReview": {
|
||||
@@ -1331,6 +1533,363 @@
|
||||
"overriddenGlobal": "Перезаписано (глобально)",
|
||||
"overriddenGlobalTooltip": "Эта камера перезаписывает глобальные настройки в этой секции",
|
||||
"overriddenBaseConfig": "Перезаписано (базовые настройки)",
|
||||
"overriddenBaseConfigTooltip": "Перезаписи настроек профиля {{profile}} в этой секции"
|
||||
"overriddenBaseConfigTooltip": "Перезаписи настроек профиля {{profile}} в этой секции",
|
||||
"overriddenGlobalHeading_one": "Эта камера переопределяет {{count}} поле из глобальной конфигурации:",
|
||||
"overriddenGlobalHeading_few": "Эта камера переопределяет {{count}} поля из глобальной конфигурации:",
|
||||
"overriddenGlobalHeading_many": "Эта камера переопределяет {{count}} полей из глобальной конфигурации:",
|
||||
"overriddenGlobalNoDeltas": "Эта камера переопределяет глобальную конфигурацию, но значения полей не отличаются.",
|
||||
"overriddenBaseConfigHeading_one": "Профиль {{profile}} переопределяет {{count}} поле из базовой конфигурации:",
|
||||
"overriddenBaseConfigHeading_few": "Профиль {{profile}} переопределяет {{count}} поля из базовой конфигурации:",
|
||||
"overriddenBaseConfigHeading_many": "Профиль {{profile}} переопределяет {{count}} полей из базовой конфигурации:",
|
||||
"overriddenBaseConfigNoDeltas": "Профиль {{profile}} переопределяет этот раздел, но значения полей не отличаются от базовой конфигурации.",
|
||||
"overriddenInCameras": {
|
||||
"label_one": "Переопределено в {{count}} камере",
|
||||
"label_few": "Переопределено в {{count}} камерах",
|
||||
"label_many": "Переопределено в {{count}} камерах",
|
||||
"tooltip_one": "{{count}} камера переопределяет значения в этом разделе. Нажмите, чтобы увидеть подробности.",
|
||||
"tooltip_few": "{{count}} камеры переопределяет значения в этом разделе. Нажмите, чтобы увидеть подробности.",
|
||||
"tooltip_many": "{{count}} камер переопределяет значения в этом разделе. Нажмите, чтобы увидеть подробности.",
|
||||
"heading_one": "В этом глобальном разделе есть поля, переопределённые в {{count}} камере.",
|
||||
"heading_few": "В этом глобальном разделе есть поля, переопределённые в {{count}} камерах.",
|
||||
"heading_many": "В этом глобальном разделе есть поля, переопределённые в {{count}} камерах.",
|
||||
"othersField_one": "{{count}} другой",
|
||||
"othersField_few": "{{count}} других",
|
||||
"othersField_many": "{{count}} других",
|
||||
"profilePrefix": "Профиль {{profile}}: {{fields}}"
|
||||
}
|
||||
},
|
||||
"menuDot": {
|
||||
"overrideGlobal": "Этот раздел переопределяет глобальную конфигурацию",
|
||||
"overrideProfile": "Этот раздел переопределён профилем {{profile}}",
|
||||
"unsaved": "В этом разделе есть несохранённые изменения"
|
||||
},
|
||||
"saveAllPreview": {
|
||||
"title": "Изменения для сохранения",
|
||||
"triggerLabel": "Просмотр ожидающих изменений",
|
||||
"empty": "Нет ожидающих изменений.",
|
||||
"scope": {
|
||||
"label": "Область",
|
||||
"global": "Глобальная",
|
||||
"camera": "Камера: {{cameraName}}"
|
||||
},
|
||||
"profile": {
|
||||
"label": "Профиль"
|
||||
},
|
||||
"field": {
|
||||
"label": "Поле"
|
||||
},
|
||||
"value": {
|
||||
"label": "Новое значение",
|
||||
"reset": "Сбросить"
|
||||
}
|
||||
},
|
||||
"timestampPosition": {
|
||||
"tl": "Сверху слева",
|
||||
"tr": "Сверху справа",
|
||||
"bl": "Снизу слева",
|
||||
"br": "Снизу справа"
|
||||
},
|
||||
"detectorsAndModel": {
|
||||
"title": "Детекторы и модель",
|
||||
"description": "Настройте бэкенд детектора, который выполняет обнаружение объектов, и используемую модель. Изменения сохраняются вместе, чтобы детектор и модель оставались согласованными.",
|
||||
"cardTitles": {
|
||||
"detector": "Оборудование детектора",
|
||||
"model": "Модель обнаружения"
|
||||
},
|
||||
"tabs": {
|
||||
"plus": "Frigate+",
|
||||
"custom": "Пользовательская модель"
|
||||
},
|
||||
"mismatch": {
|
||||
"warning": "Текущая модель Frigate+ «{{model}}» требует детектор {{required}}. Выберите совместимую модель ниже или переключитесь на пользовательскую модель перед сохранением."
|
||||
},
|
||||
"plusModel": {
|
||||
"requiresDetector": "Требуется: {{detector}}",
|
||||
"noModelSelected": "Выберите модель Frigate+"
|
||||
},
|
||||
"toast": {
|
||||
"saveSuccess": "Настройки детекторов и модели сохранены. Перезапустите Frigate, чтобы применить изменения.",
|
||||
"saveError": "Не удалось сохранить настройки детектора и модели"
|
||||
},
|
||||
"unsavedChanges": "Несохранённые изменения детектора и модели",
|
||||
"restartRequired": "Требуется перезапуск (изменён детектор или модель)"
|
||||
},
|
||||
"maintenance": {
|
||||
"title": "Обслуживание",
|
||||
"sync": {
|
||||
"title": "Синхронизация медиа",
|
||||
"desc": "Frigate периодически очищает медиафайлы по расписанию в соответствии с вашей конфигурацией хранения. Во время работы Frigate нормально видеть несколько потерянных файлов. Используйте эту функцию, чтобы удалить с диска потерянные медиафайлы, на которые больше нет ссылок в базе данных.",
|
||||
"started": "Синхронизация мультимедиа началась.",
|
||||
"alreadyRunning": "Задание синхронизации уже запущено",
|
||||
"error": "Не удалось начать синхронизацию",
|
||||
"currentStatus": "Статус",
|
||||
"jobId": "Идентификатор процесса",
|
||||
"startTime": "Время начала",
|
||||
"endTime": "Конец времени",
|
||||
"statusLabel": "Статус",
|
||||
"results": "Результаты",
|
||||
"errorLabel": "Ошибка",
|
||||
"mediaTypes": "Типы носителей",
|
||||
"allMedia": "Все носители",
|
||||
"dryRun": "Пробный прогон",
|
||||
"dryRunEnabled": "Никакие файлы не будут удалены",
|
||||
"dryRunDisabled": "Файлы будут удалены",
|
||||
"force": "Принудительно",
|
||||
"forceDesc": "Обойти порог безопасности и завершить синхронизацию, даже если будет удалено более 50% файлов.",
|
||||
"verbose": "Детали",
|
||||
"verboseDesc": "Запишите полный список потерянных файлов на диск для просмотра.",
|
||||
"running": "Синхронизация выполняется…",
|
||||
"start": "Начать синхронизацию",
|
||||
"inProgress": "Синхронизация выполняется. Эта страница отключена.",
|
||||
"status": {
|
||||
"queued": "В очереди",
|
||||
"running": "Выполняется",
|
||||
"completed": "Завершено",
|
||||
"failed": "Ошибка",
|
||||
"notRunning": "Не выполняется"
|
||||
},
|
||||
"resultsFields": {
|
||||
"filesChecked": "Файлы проверены",
|
||||
"orphansFound": "Найдены потерянные файлы",
|
||||
"orphansDeleted": "Удалены потерянные файлы",
|
||||
"aborted": "Прервано. Удаление превысит порог безопасности.",
|
||||
"error": "Ошибка",
|
||||
"totals": "Итоги"
|
||||
},
|
||||
"event_snapshots": "Снимки отслеживаемых объектов",
|
||||
"event_thumbnails": "Миниатюры отслеживаемых объектов",
|
||||
"review_thumbnails": "Обзор миниатюр",
|
||||
"previews": "Предпросмотры",
|
||||
"exports": "Экспорт",
|
||||
"recordings": "Записи"
|
||||
},
|
||||
"regionGrid": {
|
||||
"title": "Сетка регионов",
|
||||
"desc": "Сетка регионов — это оптимизация, которая запоминает, где в кадре камеры обычно появляются объекты разного размера. Frigate использует эти данные, чтобы эффективнее подбирать размер областей обнаружения. Сетка автоматически строится со временем на основе данных отслеживаемых объектов.",
|
||||
"clear": "Очистить сетку регионов",
|
||||
"clearConfirmTitle": "Очистить сетку регионов",
|
||||
"clearConfirmDesc": "Очищать сетку регионов не рекомендуется, если только вы недавно не изменили размер модели детектора или не изменили физическое положение камеры и не возникли проблемы с отслеживанием объектов. Сетка будет автоматически перестраиваться со временем по мере отслеживания объектов. Чтобы изменения вступили в силу, требуется перезапуск Frigate.",
|
||||
"clearSuccess": "Сетка регионов успешно очищена",
|
||||
"clearError": "Не удалось очистить сетку регионов",
|
||||
"restartRequired": "Требуется перезапуск, чтобы изменения сетки регионов вступили в силу"
|
||||
}
|
||||
},
|
||||
"configForm": {
|
||||
"global": {
|
||||
"title": "Глобальные настройки",
|
||||
"description": "Эти настройки применяются ко всем камерам, если они не переопределены в настройках конкретной камеры."
|
||||
},
|
||||
"camera": {
|
||||
"title": "Настройки камеры",
|
||||
"description": "Эти настройки применяются только к этой камере и переопределяют глобальные настройки.",
|
||||
"noCameras": "Нет доступных камер"
|
||||
},
|
||||
"advancedSettingsCount": "Расширенные настройки ({{count}})",
|
||||
"advancedCount": "Расширенный ({{count}})",
|
||||
"showAdvanced": "Показать дополнительные настройки",
|
||||
"tabs": {
|
||||
"sharedDefaults": "Общие настройки по умолчанию",
|
||||
"system": "Система",
|
||||
"integrations": "Интеграции"
|
||||
},
|
||||
"additionalProperties": {
|
||||
"keyLabel": "Ключ",
|
||||
"valueLabel": "Значение",
|
||||
"keyPlaceholder": "Новый ключ",
|
||||
"remove": "Удалить",
|
||||
"providerNameLabel": "Имя провайдера",
|
||||
"providerNamePlaceholder": "например, openai",
|
||||
"variableNameLabel": "Имя переменной",
|
||||
"variableNamePlaceholder": "например, MY_VARIABLE",
|
||||
"loggerNameLabel": "Имя логгера",
|
||||
"loggerNamePlaceholder": "например, frigate.record",
|
||||
"keyPatternError": "Используйте только буквы, цифры, дефисы и подчёркивания (без пробелов)"
|
||||
},
|
||||
"knownPlates": {
|
||||
"namePlaceholder": "например, Машина жены",
|
||||
"platePlaceholder": "Номерной знак или regex"
|
||||
},
|
||||
"liveStreams": {
|
||||
"streamNameLabel": "Название потока",
|
||||
"streamNamePlaceholder": "например, основной поток HD",
|
||||
"go2rtcStreamLabel": "go2rtc поток",
|
||||
"go2rtcStreamPlaceholder": "Выберите поток go2rtc",
|
||||
"go2rtcStreamSearch": "Найдите или введите название потока…",
|
||||
"noGo2rtcStreams": "Потоки go2rtc не настроены",
|
||||
"availableStreams": "Доступные потоки",
|
||||
"useCustom": "Используйте «{{value}}»",
|
||||
"addStream": "Добавить поток"
|
||||
},
|
||||
"timezone": {
|
||||
"defaultOption": "Использовать часовой пояс браузера"
|
||||
},
|
||||
"roleMap": {
|
||||
"empty": "Нет сопоставлений ролей",
|
||||
"roleLabel": "Роль",
|
||||
"groupsLabel": "Группы",
|
||||
"addMapping": "Добавить сопоставление ролей",
|
||||
"remove": "Удалить"
|
||||
},
|
||||
"ffmpegArgs": {
|
||||
"preset": "Предустановка",
|
||||
"manual": "Ручные аргументы",
|
||||
"inherit": "Наследовать настройки камеры",
|
||||
"none": "Нет",
|
||||
"useGlobalSetting": "Наследовать от глобальной настройки",
|
||||
"selectPreset": "Выберите предустановку",
|
||||
"manualPlaceholder": "Введите аргументы FFmpeg",
|
||||
"presetLabels": {
|
||||
"preset-rpi-64-h264": "Raspberry Pi (H.264)",
|
||||
"preset-rpi-64-h265": "Raspberry Pi (H.265)",
|
||||
"preset-vaapi": "VAAPI (Intel/AMD GPU)",
|
||||
"preset-intel-qsv-h264": "Intel QuickSync (H.264)",
|
||||
"preset-intel-qsv-h265": "Intel QuickSync (H.265)",
|
||||
"preset-nvidia": "NVIDIA GPU",
|
||||
"preset-jetson-h264": "NVIDIA Jetson (H.264)",
|
||||
"preset-jetson-h265": "NVIDIA Jetson (H.265)",
|
||||
"preset-rkmpp": "Rockchip RKMPP",
|
||||
"preset-http-jpeg-generic": "HTTP JPEG (универсальный)",
|
||||
"preset-http-mjpeg-generic": "HTTP MJPEG (универсальный)",
|
||||
"preset-http-reolink": "HTTP — камеры Reolink",
|
||||
"preset-rtmp-generic": "RTMP (универсальный)",
|
||||
"preset-rtsp-generic": "RTSP (универсальный)",
|
||||
"preset-rtsp-restream": "RTSP — рестрим из go2rtc",
|
||||
"preset-rtsp-restream-low-latency": "RTSP — рестрим из go2rtc (низкая задержка)",
|
||||
"preset-rtsp-udp": "RTSP — UDP",
|
||||
"preset-rtsp-blue-iris": "RTSP — Blue Iris",
|
||||
"preset-record-generic": "Запись (универсальная, без звука)",
|
||||
"preset-record-generic-audio-copy": "Запись (универсальная + копирование аудио)",
|
||||
"preset-record-generic-audio-aac": "Запись (универсальная + аудио в AAC)",
|
||||
"preset-record-mjpeg": "Запись — камеры MJPEG",
|
||||
"preset-record-jpeg": "Запись — камеры JPEG",
|
||||
"preset-record-ubiquiti": "Запись — камеры Ubiquiti"
|
||||
}
|
||||
},
|
||||
"cameraInputs": {
|
||||
"itemTitle": "Поток {{index}}",
|
||||
"sourceMode": {
|
||||
"restream": "Рестрим (go2rtc)",
|
||||
"manual": "Путь ввода вручную",
|
||||
"go2rtcStreamLabel": "go2rtc поток",
|
||||
"go2rtcStreamPlaceholder": "Выберите поток go2rtc",
|
||||
"noGo2rtcStreams": "Потоки go2rtc не настроены",
|
||||
"go2rtcStreamSearch": "Поиск потоков…",
|
||||
"availableStreams": "Доступные потоки",
|
||||
"noMatchingStreams": "Нет подходящих потоков"
|
||||
}
|
||||
},
|
||||
"restartRequiredField": "Требуется перезапуск",
|
||||
"restartRequiredFooter": "Конфигурация изменена – требуется перезагрузка",
|
||||
"sections": {
|
||||
"detect": "Обнаружение",
|
||||
"record": "Запись",
|
||||
"snapshots": "Снимки",
|
||||
"motion": "Движение",
|
||||
"objects": "Объекты",
|
||||
"review": "Обзор",
|
||||
"audio": "Аудио",
|
||||
"notifications": "Уведомления",
|
||||
"live": "Просмотр в реальном времени",
|
||||
"timestamp_style": "Временные метки",
|
||||
"mqtt": "MQTT",
|
||||
"database": "База данных",
|
||||
"telemetry": "Телеметрия",
|
||||
"auth": "Аутентификация",
|
||||
"tls": "TLS",
|
||||
"proxy": "Прокси",
|
||||
"go2rtc": "go2rtc",
|
||||
"ffmpeg": "FFmpeg",
|
||||
"detectors": "Детекторы",
|
||||
"model": "Модель",
|
||||
"semantic_search": "Семантический поиск",
|
||||
"genai": "GenAI",
|
||||
"face_recognition": "Распознавание лиц",
|
||||
"lpr": "Распознавание номерных знаков",
|
||||
"birdseye": "Birdseye",
|
||||
"masksAndZones": "Маски / Зоны"
|
||||
},
|
||||
"detect": {
|
||||
"title": "Настройки обнаружения"
|
||||
},
|
||||
"detectors": {
|
||||
"title": "Настройки детектора",
|
||||
"singleType": "Разрешен только один детектор {{type}}.",
|
||||
"keyRequired": "Требуется имя детектора.",
|
||||
"keyDuplicate": "Имя детектора уже существует.",
|
||||
"noSchema": "Схемы детекторов отсутствуют.",
|
||||
"none": "Экземпляры детектора не настроены.",
|
||||
"add": "Добавить детектор",
|
||||
"addCustomKey": "Добавить пользовательский ключ"
|
||||
},
|
||||
"record": {
|
||||
"title": "Настройки записи"
|
||||
},
|
||||
"snapshots": {
|
||||
"title": "Настройки снимка"
|
||||
},
|
||||
"motion": {
|
||||
"title": "Настройки движения"
|
||||
},
|
||||
"objects": {
|
||||
"title": "Настройки объекта"
|
||||
},
|
||||
"audioLabels": {
|
||||
"summary": "Выбраны аудиометки {{count}}",
|
||||
"empty": "Нет доступных аудиометок"
|
||||
},
|
||||
"objectLabels": {
|
||||
"summary": "Выбраны типы объектов {{count}}.",
|
||||
"empty": "Нет доступных меток объектов"
|
||||
},
|
||||
"reviewLabels": {
|
||||
"summary": "Выбраны ярлыки {{count}}",
|
||||
"empty": "Нет доступных ярлыков"
|
||||
},
|
||||
"filters": {
|
||||
"objectFieldLabel": "{{field}} для {{label}}"
|
||||
},
|
||||
"zoneNames": {
|
||||
"summary": "{{count}} выбрано",
|
||||
"empty": "Нет доступных зон"
|
||||
},
|
||||
"inputRoles": {
|
||||
"summary": "Выбраны роли {{count}}",
|
||||
"empty": "Нет доступных ролей",
|
||||
"options": {
|
||||
"detect": "Обнаружение",
|
||||
"record": "Запись",
|
||||
"audio": "Аудио"
|
||||
}
|
||||
},
|
||||
"genaiRoles": {
|
||||
"options": {
|
||||
"embeddings": "Встраивание",
|
||||
"descriptions": "Описания",
|
||||
"chat": "Чат"
|
||||
}
|
||||
},
|
||||
"semanticSearchModel": {
|
||||
"placeholder": "Выберите модель…",
|
||||
"builtIn": "Встроенные модели",
|
||||
"genaiProviders": "Поставщики GenAI"
|
||||
},
|
||||
"semanticSearchModelSize": {
|
||||
"notApplicable": "Не применимо для провайдеров GenAI"
|
||||
},
|
||||
"review": {
|
||||
"title": "Просмотр настроек"
|
||||
},
|
||||
"audio": {
|
||||
"title": "Настройки звука"
|
||||
},
|
||||
"notifications": {
|
||||
"title": "Настройки уведомлений"
|
||||
},
|
||||
"live": {
|
||||
"title": "Настройки просмотра в реальном времени"
|
||||
},
|
||||
"timestamp_style": {
|
||||
"title": "Настройки временной метки"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1 +1,27 @@
|
||||
{}
|
||||
{
|
||||
"speech": "Fjalim",
|
||||
"babbling": "Grumbullim",
|
||||
"yell": "Bërtas",
|
||||
"whispering": "Pëshpëritje",
|
||||
"laughter": "E qeshur",
|
||||
"snicker": "Qeshje e lehtë",
|
||||
"crying": "Qarje",
|
||||
"sigh": "Psherëtimë",
|
||||
"singing": "Këndim",
|
||||
"choir": "Kor",
|
||||
"child_singing": "Fëmijët këndojnë",
|
||||
"synthetic_singing": "Këndim sintetik",
|
||||
"rapping": "Rap",
|
||||
"humming": "Gumëzhimë",
|
||||
"groan": "Rënkim",
|
||||
"grunt": "Përgjërim",
|
||||
"whistling": "Fishkëllima",
|
||||
"breathing": "Frymëmarrja",
|
||||
"wheeze": "Fryme me fishkëllimë",
|
||||
"snoring": "Gërhitje",
|
||||
"whoop": "Klithmë",
|
||||
"chant": "Këndim ritmik",
|
||||
"mantra": "Është një fjalë, tingull ose frazë e shenjtë që përsëritet gjatë meditimit apo lutjes për të ndihmuar përqendrimin",
|
||||
"yodeling": "Këndim me jodlim",
|
||||
"bellow": "Ulërimë, rrahje zëri"
|
||||
}
|
||||
|
||||
@@ -97,7 +97,7 @@
|
||||
"timeRange": "Tidsintervall",
|
||||
"selectFromTimeline": "Välj från tidslinjen",
|
||||
"cameraSelection": "Kameror",
|
||||
"cameraSelectionHelp": "Kameror med spårade objekt inom tidsintervallet är förvalda.",
|
||||
"cameraSelectionHelp": "Kameror med spårade objekt inom tidsintervallet är förvalda",
|
||||
"checkingActivity": "Kontrollerar kameraaktivitet…",
|
||||
"noCameras": "Inga kameror tillgängliga",
|
||||
"detectionCount_one": "1 spårat objekt",
|
||||
|
||||
@@ -12,7 +12,8 @@
|
||||
"motionTuner": "Rörelsejustering - Frigate",
|
||||
"object": "Felsökning - Frigate",
|
||||
"cameraManagement": "Hantera kameror - Frigate",
|
||||
"cameraReview": "Kameragranskningsinställningar - Frigate"
|
||||
"cameraReview": "Kameragranskningsinställningar - Frigate",
|
||||
"profiles": "Profiler - Frigate"
|
||||
},
|
||||
"general": {
|
||||
"title": "Inställningar för användargränssnittet",
|
||||
@@ -152,7 +153,12 @@
|
||||
"roles": "Roller",
|
||||
"cameraManagement": "Kamerahantering",
|
||||
"cameraReview": "Granskning",
|
||||
"profiles": "Profiler"
|
||||
"profiles": "Profiler",
|
||||
"maintenance": "Underhåll",
|
||||
"cameraObjects": "Föremål",
|
||||
"cameraUi": "Kameragränssnitt",
|
||||
"cameraFaceRecognition": "Ansiktsigenkänning",
|
||||
"cameraRecording": "Inspelning"
|
||||
},
|
||||
"dialog": {
|
||||
"unsavedChanges": {
|
||||
@@ -1240,5 +1246,13 @@
|
||||
"success": "Konfigurationen för granskning av klassificering har sparats. Starta om Frigate för att tillämpa ändringarna."
|
||||
}
|
||||
}
|
||||
},
|
||||
"saveAllPreview": {
|
||||
"value": {
|
||||
"reset": "Återställ"
|
||||
},
|
||||
"field": {
|
||||
"label": "Fält"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1 +1,31 @@
|
||||
{}
|
||||
{
|
||||
"speech": "ప్రసంగం",
|
||||
"yell": "అరువు",
|
||||
"whispering": "గుసగుసలాడు",
|
||||
"laughter": "నవ్వు",
|
||||
"crying": "ఏడుపు",
|
||||
"singing": "పాడు",
|
||||
"snoring": "గురకపెట్టు",
|
||||
"child_singing": "పాప పాట పాడుట",
|
||||
"cough": "దగ్గుట",
|
||||
"throat_clearing": "గొంతు సవరించు కొను",
|
||||
"sneeze": "తుమ్ముట",
|
||||
"run": "పరిగెత్తుట",
|
||||
"chewing": "నవులుట",
|
||||
"biting": "కొరుకుట",
|
||||
"gargling": "పుక్కిలించుట",
|
||||
"burping": "త్రెంచుట",
|
||||
"hiccup": "ఎక్కిళ్ళు",
|
||||
"fart": "పిత్తు",
|
||||
"hands": "చేతులు",
|
||||
"finger_snapping": "వెళ్ళు విరుచుట",
|
||||
"clapping": "చప్పట్లు కొట్టుట",
|
||||
"animal": "జంతువు",
|
||||
"pets": "పెంపుడు జెంతువులు",
|
||||
"dog": "కుక్క",
|
||||
"bark": "మొరుగు",
|
||||
"bow_wow": "భౌ వౌ",
|
||||
"cat": "పిల్లి",
|
||||
"meow": "మియావ్",
|
||||
"horse": "గుర్రం"
|
||||
}
|
||||
|
||||
@@ -1 +1,7 @@
|
||||
{}
|
||||
{
|
||||
"animal": "జంతువు",
|
||||
"dog": "కుక్క",
|
||||
"bark": "మొరుగు",
|
||||
"cat": "పిల్లి",
|
||||
"horse": "గుర్రం"
|
||||
}
|
||||
|
||||
@@ -534,7 +534,7 @@
|
||||
"description": "控制界面中显示的录像预览质量的设置。",
|
||||
"quality": {
|
||||
"label": "预览质量",
|
||||
"description": "预览质量级别(very_low、low、medium、high、very_high)。"
|
||||
"description": "预览质量级别(非常低、低、中等、高、非常高)。"
|
||||
}
|
||||
},
|
||||
"enabled_in_config": {
|
||||
@@ -859,8 +859,8 @@
|
||||
"description": "用于在页面中排序摄像头的顺序(只会影响默认仪表板和列表);数值越大则在越后面。"
|
||||
},
|
||||
"dashboard": {
|
||||
"label": "在页面中显示",
|
||||
"description": "切换此摄像头在 Frigate 页面的所有位置是否可见。禁用此项将需要手动编辑配置才能在页面中再次查看此摄像头。"
|
||||
"label": "在实时监控面板中显示",
|
||||
"description": "切换此摄像头是否在默认的“所有摄像头”实时监控仪表盘上可见。该摄像头在页面的其他位置(例如摄像头分组和设置页)仍然可见。"
|
||||
},
|
||||
"review": {
|
||||
"label": "在核查中显示",
|
||||
|
||||
@@ -1405,7 +1405,7 @@
|
||||
},
|
||||
"model_type": {
|
||||
"label": "目标检测模型类型",
|
||||
"description": "某些检测器用于优化的检测器模型架构类型(ssd、yolox、yolonas)。"
|
||||
"description": "某些检测器用于优化的检测器模型架构类型(ssd、yolox、yolonas、yolo-generic、rfdetr和dfine)。"
|
||||
}
|
||||
},
|
||||
"model_path": {
|
||||
@@ -1454,7 +1454,7 @@
|
||||
},
|
||||
"model_type": {
|
||||
"label": "目标检测模型类型",
|
||||
"description": "某些检测器用于优化的检测器模型架构类型(ssd、yolox、yolonas)。"
|
||||
"description": "某些检测器用于优化的检测器模型架构类型(ssd、yolox、yolonas、yolo-generic、rfdetr和dfine)。"
|
||||
}
|
||||
},
|
||||
"genai": {
|
||||
@@ -1788,7 +1788,7 @@
|
||||
"description": "控制界面中显示的录像预览质量的设置。",
|
||||
"quality": {
|
||||
"label": "预览质量",
|
||||
"description": "预览质量级别(very_low、low、medium、high、very_high)。"
|
||||
"description": "预览质量级别(非常低、低、中等、高、非常高)。"
|
||||
}
|
||||
},
|
||||
"enabled_in_config": {
|
||||
@@ -2214,8 +2214,8 @@
|
||||
"description": "用于在页面中排序摄像头的顺序(只会影响默认仪表板和列表);数值越大则在越后面。"
|
||||
},
|
||||
"dashboard": {
|
||||
"label": "在页面中显示",
|
||||
"description": "切换此摄像头在 Frigate 页面中是否可见。禁用后需要手动编辑配置才能再次在页面中查看此摄像头。"
|
||||
"label": "在实时监控仪表盘中显示",
|
||||
"description": "切换此摄像头是否在默认的“所有摄像头”实时监控仪表盘上可见。该摄像头在页面的其他位置(例如摄像头分组和设置页)仍然可见。"
|
||||
},
|
||||
"review": {
|
||||
"label": "在核查中显示",
|
||||
|
||||
@@ -126,5 +126,7 @@
|
||||
"baby_stroller": "婴儿车",
|
||||
"rickshaw": "三轮车",
|
||||
"Rodent": "啮齿动物",
|
||||
"rodent": "鼠类动物"
|
||||
"rodent": "鼠类动物",
|
||||
"possum": "负鼠",
|
||||
"garbage_truck": "垃圾清运车"
|
||||
}
|
||||
|
||||
@@ -1388,7 +1388,7 @@
|
||||
"webuiUrlHelp": "从调试页面中直接访问摄像头管理网页界面。留空以禁用链接。",
|
||||
"webuiUrlInvalid": "必须是有效的 URL(例如:https://example.com)。",
|
||||
"dashboardLabel": "在实时监控面板上显示",
|
||||
"dashboardHelp": "在实时监控面板显示该摄像头。",
|
||||
"dashboardHelp": "在实时监控面板显示该摄像头,在分组等其他位置仍可见。",
|
||||
"reviewLabel": "在核查中显示",
|
||||
"reviewHelp": "在核查页面展示该摄像头,包括在筛选列表、画面变动核查以及历史视图。"
|
||||
},
|
||||
@@ -2134,7 +2134,8 @@
|
||||
"autotrackingNoZones": "自动追踪至少需要一个区域。请先在“遮罩 / 区域”中为此摄像头定义一个区域,然后在下方将其设置为必需区域。"
|
||||
},
|
||||
"ffmpeg": {
|
||||
"hwaccelManualNotRecommended": "不建议手动硬件加速参数。除非存在特定需求,否则选择与你的硬件匹配的预设。"
|
||||
"hwaccelManualNotRecommended": "不建议手动硬件加速参数。除非存在特定需求,否则选择与你的硬件匹配的预设。",
|
||||
"inputsMissingGo2rtcStream": "下面的输入指向了一个已不存在的 go2rtc 转流。请选择一个现有的转流或手动输入摄像头的 URL,否则此摄像头将无法连接。"
|
||||
},
|
||||
"model": {
|
||||
"optimizedFor320": "Frigate 针对了 320x320 的模型进行优化,这是大多数设置的最佳选择。640x640 模型速度较慢,仅在特定场景下有帮助。",
|
||||
|
||||
@@ -37,7 +37,7 @@ export function LogSettingsButton({
|
||||
</Button>
|
||||
);
|
||||
const content = (
|
||||
<div className={cn("my-3 space-y-3 py-3 md:mt-0 md:py-0")}>
|
||||
<div className={cn("my-3 space-y-3 px-3 py-3 md:mt-0 md:px-0 md:py-0")}>
|
||||
<div className="space-y-4">
|
||||
<div className="space-y-0.5">
|
||||
<div>{t("filter")}</div>
|
||||
@@ -77,7 +77,7 @@ export function LogSettingsButton({
|
||||
return (
|
||||
<Drawer>
|
||||
<DrawerTrigger asChild>{trigger}</DrawerTrigger>
|
||||
<DrawerContent className="mx-1 max-h-[75dvh] overflow-hidden p-3">
|
||||
<DrawerContent className="mx-1 max-h-[75dvh] overflow-hidden">
|
||||
{content}
|
||||
</DrawerContent>
|
||||
</Drawer>
|
||||
|
||||
@@ -1,16 +1,20 @@
|
||||
import { baseUrl } from "@/api/baseUrl";
|
||||
import { useCallback, useRef } from "react";
|
||||
import { CameraConfig } from "@/types/frigateConfig";
|
||||
import { useCallback, useMemo, useRef } from "react";
|
||||
|
||||
const GRID_SIZE = 16;
|
||||
const DEFAULT_ASPECT_RATIO = 16 / 9;
|
||||
// Cap how tall the grid can get for portrait and 4:3 cameras
|
||||
const MAX_GRID_HEIGHT = "65dvh";
|
||||
|
||||
type MotionRegionFilterGridProps = {
|
||||
cameraName: string;
|
||||
camera: CameraConfig;
|
||||
selectedCells: Set<number>;
|
||||
onCellsChange: (cells: Set<number>) => void;
|
||||
};
|
||||
|
||||
export default function MotionRegionFilterGrid({
|
||||
cameraName,
|
||||
camera,
|
||||
selectedCells,
|
||||
onCellsChange,
|
||||
}: MotionRegionFilterGridProps) {
|
||||
@@ -21,6 +25,18 @@ export default function MotionRegionFilterGrid({
|
||||
const lastCellRef = useRef<number>(-1);
|
||||
const gridRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
// Cells are indexed against the detect frame, so the grid has to match the
|
||||
// frame's aspect ratio or painted cells land on the wrong part of the image
|
||||
const aspectRatio = useMemo(() => {
|
||||
if (!camera.detect.width || !camera.detect.height) {
|
||||
return DEFAULT_ASPECT_RATIO;
|
||||
}
|
||||
|
||||
const ratio = camera.detect.width / camera.detect.height;
|
||||
|
||||
return Number.isFinite(ratio) && ratio > 0 ? ratio : DEFAULT_ASPECT_RATIO;
|
||||
}, [camera.detect.height, camera.detect.width]);
|
||||
|
||||
const toggleCell = useCallback(
|
||||
(index: number, forceAdd?: boolean) => {
|
||||
const next = new Set(selectedCells);
|
||||
@@ -109,13 +125,17 @@ export default function MotionRegionFilterGrid({
|
||||
return (
|
||||
<div className="space-y-2">
|
||||
<div
|
||||
className="relative aspect-video w-full select-none overflow-hidden rounded-lg"
|
||||
style={{ touchAction: "none" }}
|
||||
className="relative mx-auto select-none overflow-hidden rounded-lg"
|
||||
style={{
|
||||
aspectRatio,
|
||||
width: `min(100%, calc(${MAX_GRID_HEIGHT} * ${aspectRatio}))`,
|
||||
touchAction: "none",
|
||||
}}
|
||||
onPointerUp={handlePointerUp}
|
||||
onPointerLeave={handlePointerUp}
|
||||
>
|
||||
<img
|
||||
src={`${baseUrl}api/${cameraName}/latest.jpg?h=500`}
|
||||
src={`${baseUrl}api/${camera.name}/latest.jpg?h=500`}
|
||||
className="absolute inset-0 size-full object-contain"
|
||||
draggable={false}
|
||||
alt=""
|
||||
|
||||
@@ -26,6 +26,7 @@ import PlatformAwareDialog from "../overlay/dialog/PlatformAwareDialog";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { getTranslatedLabel } from "@/utils/i18n";
|
||||
import { useAllowedCameras } from "@/hooks/use-allowed-cameras";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const REVIEW_FILTERS = [
|
||||
"cameras",
|
||||
@@ -409,6 +410,7 @@ function GeneralFilterButton({
|
||||
onUpdateFilter(resetFilter);
|
||||
}}
|
||||
onClose={() => setOpen(false)}
|
||||
contentClassName="p-4"
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -416,6 +418,7 @@ function GeneralFilterButton({
|
||||
<PlatformAwareDialog
|
||||
trigger={trigger}
|
||||
content={content}
|
||||
contentClassName="p-1"
|
||||
open={open}
|
||||
onOpenChange={(open) => {
|
||||
if (!open) {
|
||||
@@ -444,6 +447,7 @@ type GeneralFilterContentProps = {
|
||||
onApply: () => void;
|
||||
onReset: () => void;
|
||||
onClose: () => void;
|
||||
contentClassName?: string;
|
||||
};
|
||||
export function GeneralFilterContent({
|
||||
allLabels,
|
||||
@@ -454,6 +458,7 @@ export function GeneralFilterContent({
|
||||
onApply,
|
||||
onReset,
|
||||
onClose,
|
||||
contentClassName,
|
||||
}: GeneralFilterContentProps) {
|
||||
const { t } = useTranslation(["components/filter", "views/events"]);
|
||||
const { data: config } = useSWR<FrigateConfig>("config", {
|
||||
@@ -476,7 +481,12 @@ export function GeneralFilterContent({
|
||||
}, [config]);
|
||||
return (
|
||||
<>
|
||||
<div className="scrollbar-container h-auto max-h-[80dvh] overflow-y-auto overflow-x-hidden">
|
||||
<div
|
||||
className={cn(
|
||||
"scrollbar-container h-auto max-h-[80dvh] overflow-y-auto overflow-x-hidden",
|
||||
contentClassName,
|
||||
)}
|
||||
>
|
||||
{currentSeverity && (
|
||||
<div className="my-2.5 flex flex-col gap-2.5">
|
||||
<FilterSwitch
|
||||
|
||||
@@ -8,6 +8,7 @@ import { Label } from "../ui/label";
|
||||
import { Switch } from "../ui/switch";
|
||||
import { DropdownMenuSeparator } from "../ui/dropdown-menu";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
type ZoneMaskFilterButtonProps = {
|
||||
selectedZoneMask?: PolygonType[];
|
||||
@@ -46,8 +47,12 @@ export function ZoneMaskFilterButton({
|
||||
return (
|
||||
<Drawer>
|
||||
<DrawerTrigger asChild>{trigger}</DrawerTrigger>
|
||||
<DrawerContent className="mx-1 max-h-[75dvh] overflow-hidden p-3">
|
||||
{content}
|
||||
<DrawerContent className="mx-1 max-h-[75dvh] overflow-hidden">
|
||||
<GeneralFilterContent
|
||||
selectedZoneMask={selectedZoneMask}
|
||||
updateZoneMaskFilter={updateZoneMaskFilter}
|
||||
contentClassName="p-3"
|
||||
/>
|
||||
</DrawerContent>
|
||||
</Drawer>
|
||||
);
|
||||
@@ -64,15 +69,22 @@ export function ZoneMaskFilterButton({
|
||||
type GeneralFilterContentProps = {
|
||||
selectedZoneMask: PolygonType[] | undefined;
|
||||
updateZoneMaskFilter: (labels: PolygonType[] | undefined) => void;
|
||||
contentClassName?: string;
|
||||
};
|
||||
export function GeneralFilterContent({
|
||||
selectedZoneMask,
|
||||
updateZoneMaskFilter,
|
||||
contentClassName,
|
||||
}: GeneralFilterContentProps) {
|
||||
const { t } = useTranslation(["components/filter"]);
|
||||
return (
|
||||
<>
|
||||
<div className="h-auto overflow-y-auto overflow-x-hidden">
|
||||
<div
|
||||
className={cn(
|
||||
"h-auto overflow-y-auto overflow-x-hidden",
|
||||
contentClassName,
|
||||
)}
|
||||
>
|
||||
<div className="my-2.5 flex items-center justify-between">
|
||||
<Label
|
||||
className="mx-2 cursor-pointer text-primary"
|
||||
|
||||
@@ -257,7 +257,7 @@ export default function GeneralSettings({
|
||||
className={
|
||||
isDesktop
|
||||
? "scrollbar-container mr-5 w-72 overflow-y-auto"
|
||||
: "max-h-[75dvh] overflow-hidden p-2"
|
||||
: "max-h-[75dvh] overflow-hidden"
|
||||
}
|
||||
>
|
||||
{!isDesktop && (
|
||||
@@ -270,7 +270,12 @@ export default function GeneralSettings({
|
||||
</DrawerDescription>
|
||||
</>
|
||||
)}
|
||||
<div className="scrollbar-container w-full flex-col overflow-y-auto overflow-x-hidden">
|
||||
<div
|
||||
className={cn(
|
||||
"scrollbar-container w-full flex-col overflow-y-auto overflow-x-hidden",
|
||||
!isDesktop && "p-2",
|
||||
)}
|
||||
>
|
||||
{isMobile && (
|
||||
<div className="mb-2">
|
||||
<DropdownMenuLabel>
|
||||
|
||||
@@ -174,11 +174,11 @@ function StatusAlertNav({ className, large }: StatusAlertNavProps) {
|
||||
</DrawerTrigger>
|
||||
<DrawerContent
|
||||
className={cn(
|
||||
"mx-1 max-h-[75dvh] overflow-hidden rounded-t-2xl px-2",
|
||||
"mx-1 max-h-[75dvh] overflow-hidden rounded-t-2xl",
|
||||
className,
|
||||
)}
|
||||
>
|
||||
<div className="scrollbar-container flex h-auto w-full flex-col items-center gap-2 overflow-y-auto overflow-x-hidden py-4">
|
||||
<div className="scrollbar-container flex h-auto w-full flex-col items-center gap-2 overflow-y-auto overflow-x-hidden px-2 py-4">
|
||||
{Object.entries(messages).map(([key, messageArray]) => (
|
||||
<div key={key} className="flex w-full items-center gap-2">
|
||||
{messageArray.map(({ id, text, color, link }: StatusMessage) => {
|
||||
|
||||
@@ -34,8 +34,8 @@ export default function MobileCameraDrawer({
|
||||
<FaVideo className="text-secondary-foreground" />
|
||||
</Button>
|
||||
</DrawerTrigger>
|
||||
<DrawerContent className="mx-1 max-h-[75dvh] overflow-hidden rounded-t-2xl px-4">
|
||||
<div className="scrollbar-container flex h-auto w-full flex-col items-center gap-2 overflow-y-auto overflow-x-hidden py-4">
|
||||
<DrawerContent className="mx-1 max-h-[75dvh] overflow-hidden rounded-t-2xl">
|
||||
<div className="scrollbar-container flex h-auto w-full flex-col items-center gap-2 overflow-y-auto overflow-x-hidden p-4">
|
||||
{allCameras.map((cam) => (
|
||||
<div
|
||||
key={cam}
|
||||
|
||||
@@ -30,6 +30,7 @@ import { useNavigate } from "react-router-dom";
|
||||
import { StartExportResponse } from "@/types/export";
|
||||
import { ShareTimestampContent } from "./ShareTimestampDialog";
|
||||
import { useIsAdmin } from "@/hooks/use-is-admin";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
type DrawerMode =
|
||||
| "none"
|
||||
@@ -518,9 +519,9 @@ export default function MobileReviewSettingsDrawer({
|
||||
} else if (drawerMode == "filter") {
|
||||
content = (
|
||||
<div className="scrollbar-container flex h-auto w-full flex-col overflow-y-auto overflow-x-hidden">
|
||||
<div className="relative mb-2 h-8 w-full">
|
||||
<div className="relative mb-4 h-8 w-full">
|
||||
<div
|
||||
className="absolute left-0 text-selected"
|
||||
className="absolute left-4 text-selected"
|
||||
onClick={() => setDrawerMode("select")}
|
||||
>
|
||||
{t("button.back", { ns: "common" })}
|
||||
@@ -548,6 +549,7 @@ export default function MobileReviewSettingsDrawer({
|
||||
onUpdateFilter(resetFilter);
|
||||
}}
|
||||
onClose={() => setDrawerMode("select")}
|
||||
contentClassName="px-4"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
@@ -685,7 +687,14 @@ export default function MobileReviewSettingsDrawer({
|
||||
</Button>
|
||||
</DrawerTrigger>
|
||||
<DrawerContent
|
||||
className={`mx-1 flex max-h-[80dvh] flex-col items-center gap-2 rounded-t-2xl px-4 pb-4 ${drawerMode == "export" || drawerMode == "debug-replay" ? "overflow-visible" : "overflow-hidden"}`}
|
||||
className={cn(
|
||||
"mx-1 flex max-h-[80dvh] flex-col items-center gap-2 rounded-t-2xl pb-4",
|
||||
// the filter content pads itself so its scrollbar reaches the drawer edge
|
||||
drawerMode != "filter" && "px-4",
|
||||
drawerMode == "export" || drawerMode == "debug-replay"
|
||||
? "overflow-visible"
|
||||
: "overflow-hidden",
|
||||
)}
|
||||
>
|
||||
{content}
|
||||
</DrawerContent>
|
||||
|
||||
@@ -491,7 +491,7 @@ export default function Step3StreamConfig({
|
||||
</Button>
|
||||
</div>
|
||||
</DrawerTrigger>
|
||||
<DrawerContent className="mx-1 max-h-[75dvh] overflow-hidden rounded-t-2xl px-2">
|
||||
<DrawerContent className="mx-1 max-h-[75dvh] overflow-hidden rounded-t-2xl">
|
||||
<div className="mt-2">
|
||||
<Command>
|
||||
<CommandInput
|
||||
@@ -500,7 +500,7 @@ export default function Step3StreamConfig({
|
||||
)}
|
||||
className="h-9"
|
||||
/>
|
||||
<CommandList>
|
||||
<CommandList className="px-2">
|
||||
<CommandEmpty>
|
||||
{t("cameraWizard.step3.noStreamFound")}
|
||||
</CommandEmpty>
|
||||
|
||||
@@ -327,17 +327,20 @@ export function ReviewTimeline({
|
||||
documentInstance?.addEventListener("touchmove", handleMouseMove);
|
||||
documentInstance?.addEventListener("mouseup", handleMouseUp);
|
||||
documentInstance?.addEventListener("touchend", handleMouseUp);
|
||||
documentInstance?.addEventListener("touchcancel", handleMouseUp);
|
||||
} else {
|
||||
documentInstance?.removeEventListener("mousemove", handleMouseMove);
|
||||
documentInstance?.removeEventListener("touchmove", handleMouseMove);
|
||||
documentInstance?.removeEventListener("mouseup", handleMouseUp);
|
||||
documentInstance?.removeEventListener("touchend", handleMouseUp);
|
||||
documentInstance?.removeEventListener("touchcancel", handleMouseUp);
|
||||
}
|
||||
return () => {
|
||||
documentInstance?.removeEventListener("mousemove", handleMouseMove);
|
||||
documentInstance?.removeEventListener("touchmove", handleMouseMove);
|
||||
documentInstance?.removeEventListener("mouseup", handleMouseUp);
|
||||
documentInstance?.removeEventListener("touchend", handleMouseUp);
|
||||
documentInstance?.removeEventListener("touchcancel", handleMouseUp);
|
||||
};
|
||||
}, [handleMouseMove, handleMouseUp, isDragging]);
|
||||
|
||||
|
||||
@@ -73,7 +73,9 @@ export const VirtualizedEventSegments = forwardRef<
|
||||
Math.ceil((scrollTop + clientHeight) / SEGMENT_HEIGHT) +
|
||||
OVERSCAN_COUNT,
|
||||
);
|
||||
setVisibleRange({ start, end });
|
||||
setVisibleRange((prev) =>
|
||||
prev.start === start && prev.end === end ? prev : { start, end },
|
||||
);
|
||||
}
|
||||
}, [segments.length, timelineRef]);
|
||||
|
||||
|
||||
@@ -77,7 +77,9 @@ export const VirtualizedMotionSegments = forwardRef<
|
||||
Math.ceil((scrollTop + clientHeight) / SEGMENT_HEIGHT) +
|
||||
OVERSCAN_COUNT,
|
||||
);
|
||||
setVisibleRange({ start, end });
|
||||
setVisibleRange((prev) =>
|
||||
prev.start === start && prev.end === end ? prev : { start, end },
|
||||
);
|
||||
}
|
||||
}, [segments.length, timelineRef]);
|
||||
|
||||
|
||||
@@ -47,7 +47,7 @@ const DrawerContent = React.forwardRef<
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<div className="mx-auto mt-4 h-2 w-[100px] rounded-full bg-muted" />
|
||||
<div className="mx-auto mt-4 h-2 w-[100px] shrink-0 rounded-full bg-muted" />
|
||||
{children}
|
||||
</DrawerPrimitive.Content>
|
||||
</DrawerPortal>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||
import { useTimelineUtils } from "./use-timeline-utils";
|
||||
import { FrigateConfig } from "@/types/frigateConfig";
|
||||
import useSWR from "swr";
|
||||
@@ -8,6 +8,8 @@ import { useTimeFormat } from "./use-date-utils";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import useUserInteraction from "./use-user-interaction";
|
||||
|
||||
const DRAG_STATE_COMMIT_MS = 100;
|
||||
|
||||
type DraggableElementProps = {
|
||||
contentRef: React.RefObject<HTMLElement | null>;
|
||||
timelineRef: React.RefObject<HTMLDivElement | null>;
|
||||
@@ -61,6 +63,8 @@ function useDraggableElement({
|
||||
|
||||
const [clientYPosition, setClientYPosition] = useState<number | null>(null);
|
||||
const [initialClickAdjustment, setInitialClickAdjustment] = useState(0);
|
||||
const lastDragTimeCommitRef = useRef(0);
|
||||
const pendingDragTimeRef = useRef<number | null>(null);
|
||||
const [elementScrollIntoView, setElementScrollIntoView] = useState(true);
|
||||
const [scrollEdgeSize, setScrollEdgeSize] = useState<number>();
|
||||
const [fullTimelineHeight, setFullTimelineHeight] = useState<number>();
|
||||
@@ -126,6 +130,7 @@ function useDraggableElement({
|
||||
}
|
||||
e.stopPropagation();
|
||||
setIsDragging(true);
|
||||
pendingDragTimeRef.current = null;
|
||||
|
||||
let clientY;
|
||||
if ("TouchEvent" in window && e.nativeEvent instanceof TouchEvent) {
|
||||
@@ -154,9 +159,14 @@ function useDraggableElement({
|
||||
if (isDragging) {
|
||||
setIsDragging(false);
|
||||
setInitialClickAdjustment(0);
|
||||
|
||||
if (pendingDragTimeRef.current !== null && setDraggableElementTime) {
|
||||
setDraggableElementTime(pendingDragTimeRef.current);
|
||||
pendingDragTimeRef.current = null;
|
||||
}
|
||||
}
|
||||
},
|
||||
[isDragging, setIsDragging],
|
||||
[isDragging, setIsDragging, setDraggableElementTime],
|
||||
);
|
||||
|
||||
const timestampToPixels = useCallback(
|
||||
@@ -346,9 +356,21 @@ function useDraggableElement({
|
||||
);
|
||||
|
||||
if (setDraggableElementTime) {
|
||||
setDraggableElementTime(
|
||||
targetSegmentTime + segmentDuration * (offset / segmentHeight),
|
||||
);
|
||||
const newTime =
|
||||
targetSegmentTime + segmentDuration * (offset / segmentHeight);
|
||||
const now = performance.now();
|
||||
|
||||
// don't commit on every animation frame, only commit it at a
|
||||
// set interval to avoid React's nested update limit
|
||||
if (now - lastDragTimeCommitRef.current >= DRAG_STATE_COMMIT_MS) {
|
||||
lastDragTimeCommitRef.current = now;
|
||||
pendingDragTimeRef.current = null;
|
||||
setDraggableElementTime(newTime);
|
||||
} else {
|
||||
// Hold the newest value; handleMouseUp flushes it so the
|
||||
// release still lands exactly where the handle was dropped.
|
||||
pendingDragTimeRef.current = newTime;
|
||||
}
|
||||
}
|
||||
|
||||
if (draggingAtTopEdge || draggingAtBottomEdge) {
|
||||
|
||||
@@ -8,6 +8,7 @@ function useUserInteraction({ elementRef }: UseUserInteractionProps) {
|
||||
const [userInteracting, setUserInteracting] = useState(false);
|
||||
const interactionTimeout = useRef<NodeJS.Timeout>(undefined);
|
||||
const isProgrammaticScroll = useRef(false);
|
||||
const userInteractingRef = useRef(false);
|
||||
|
||||
const setProgrammaticScroll = useCallback(() => {
|
||||
isProgrammaticScroll.current = true;
|
||||
@@ -16,13 +17,18 @@ function useUserInteraction({ elementRef }: UseUserInteractionProps) {
|
||||
useEffect(() => {
|
||||
const handleUserInteraction = () => {
|
||||
if (!isProgrammaticScroll.current) {
|
||||
setUserInteracting(true);
|
||||
// Only commit state on the leading edge
|
||||
if (!userInteractingRef.current) {
|
||||
userInteractingRef.current = true;
|
||||
setUserInteracting(true);
|
||||
}
|
||||
|
||||
if (interactionTimeout.current) {
|
||||
clearTimeout(interactionTimeout.current);
|
||||
}
|
||||
|
||||
interactionTimeout.current = setTimeout(() => {
|
||||
userInteractingRef.current = false;
|
||||
setUserInteracting(false);
|
||||
}, 3000);
|
||||
} else {
|
||||
|
||||
@@ -8,6 +8,8 @@ import EventReviewTimeline from "@/components/timeline/EventReviewTimeline";
|
||||
import ActivityIndicator from "@/components/indicators/activity-indicator";
|
||||
import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group";
|
||||
import { VolumeSlider } from "@/components/ui/slider";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Switch } from "@/components/ui/switch";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
@@ -1154,6 +1156,7 @@ function MotionReview({
|
||||
new Set(),
|
||||
);
|
||||
const [isRegionFilterOpen, setIsRegionFilterOpen] = useState(false);
|
||||
const [cropToFilter, setCropToFilter] = useState(true);
|
||||
|
||||
// reset filter when camera changes
|
||||
useEffect(() => {
|
||||
@@ -1393,7 +1396,7 @@ function MotionReview({
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<MotionRegionFilterGrid
|
||||
cameraName={selectedMotionPreviewCamera.name}
|
||||
camera={selectedMotionPreviewCamera}
|
||||
selectedCells={pendingFilterCells}
|
||||
onCellsChange={setPendingFilterCells}
|
||||
/>
|
||||
@@ -1448,7 +1451,12 @@ function MotionReview({
|
||||
|
||||
<div className="space-y-3">
|
||||
<div className="space-y-0.5">
|
||||
<div>{t("motionPreviews.speed")}</div>
|
||||
<Label
|
||||
className="cursor-pointer"
|
||||
htmlFor="motionPreviewSpeed"
|
||||
>
|
||||
{t("motionPreviews.speed")}
|
||||
</Label>
|
||||
<div className="text-xs text-muted-foreground">
|
||||
{t("motionPreviews.speedDesc")}
|
||||
</div>
|
||||
@@ -1460,6 +1468,7 @@ function MotionReview({
|
||||
}
|
||||
>
|
||||
<SelectTrigger
|
||||
id="motionPreviewSpeed"
|
||||
className="h-10 w-full"
|
||||
aria-label={t("motionPreviews.speedAria")}
|
||||
>
|
||||
@@ -1477,7 +1486,7 @@ function MotionReview({
|
||||
|
||||
<div className="space-y-3">
|
||||
<div className="space-y-0.5">
|
||||
<div>{t("motionPreviews.dim")}</div>
|
||||
<Label>{t("motionPreviews.dim")}</Label>
|
||||
<div className="text-xs text-muted-foreground">
|
||||
{t("motionPreviews.dimDesc")}
|
||||
</div>
|
||||
@@ -1502,6 +1511,26 @@ function MotionReview({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-0.5">
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<Label
|
||||
className="cursor-pointer"
|
||||
htmlFor="cropToFilter"
|
||||
>
|
||||
{t("motionPreviews.crop")}
|
||||
</Label>
|
||||
<Switch
|
||||
id="cropToFilter"
|
||||
checked={cropToFilter}
|
||||
onCheckedChange={setCropToFilter}
|
||||
aria-label={t("motionPreviews.cropAria")}
|
||||
/>
|
||||
</div>
|
||||
<div className="text-xs text-muted-foreground">
|
||||
{t("motionPreviews.cropDesc")}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{!isDesktop && (
|
||||
<>
|
||||
<SelectSeparator />
|
||||
@@ -1557,6 +1586,7 @@ function MotionReview({
|
||||
playbackRate={playbackRate}
|
||||
nonMotionAlpha={dimStrength / 100}
|
||||
motionFilterCells={motionFilterCells}
|
||||
cropToFilter={cropToFilter}
|
||||
onSeek={(timestamp) => {
|
||||
onOpenRecording({
|
||||
camera: selectedMotionPreviewCamera.name,
|
||||
|
||||
@@ -24,6 +24,94 @@ import { FrigateConfig } from "@/types/frigateConfig";
|
||||
|
||||
const MOTION_HEATMAP_GRID_SIZE = 16;
|
||||
const MIN_MOTION_CELL_ALPHA = 0.06;
|
||||
const DEFAULT_TILE_ASPECT_RATIO = 16 / 9;
|
||||
// Keep cropped tiles from collapsing into unusable slivers when the selection
|
||||
// is a single row or column
|
||||
const MIN_CROP_TILE_ASPECT_RATIO = 0.75;
|
||||
const MAX_CROP_TILE_ASPECT_RATIO = 4;
|
||||
|
||||
type CropRegion = {
|
||||
x: number;
|
||||
y: number;
|
||||
width: number;
|
||||
height: number;
|
||||
};
|
||||
|
||||
type MediaRect = {
|
||||
x: number;
|
||||
y: number;
|
||||
width: number;
|
||||
height: number;
|
||||
};
|
||||
|
||||
function getCropRegionForCells(cells?: Set<number>): CropRegion | undefined {
|
||||
if (!cells || cells.size === 0) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
let minRow = MOTION_HEATMAP_GRID_SIZE;
|
||||
let maxRow = -1;
|
||||
let minCol = MOTION_HEATMAP_GRID_SIZE;
|
||||
let maxCol = -1;
|
||||
|
||||
cells.forEach((cellIndex) => {
|
||||
if (
|
||||
!Number.isInteger(cellIndex) ||
|
||||
cellIndex < 0 ||
|
||||
cellIndex >= MOTION_HEATMAP_GRID_SIZE ** 2
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const row = Math.floor(cellIndex / MOTION_HEATMAP_GRID_SIZE);
|
||||
const col = cellIndex % MOTION_HEATMAP_GRID_SIZE;
|
||||
|
||||
minRow = Math.min(minRow, row);
|
||||
maxRow = Math.max(maxRow, row);
|
||||
minCol = Math.min(minCol, col);
|
||||
maxCol = Math.max(maxCol, col);
|
||||
});
|
||||
|
||||
if (maxRow < 0 || maxCol < 0) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return {
|
||||
x: minCol / MOTION_HEATMAP_GRID_SIZE,
|
||||
y: minRow / MOTION_HEATMAP_GRID_SIZE,
|
||||
width: (maxCol - minCol + 1) / MOTION_HEATMAP_GRID_SIZE,
|
||||
height: (maxRow - minRow + 1) / MOTION_HEATMAP_GRID_SIZE,
|
||||
};
|
||||
}
|
||||
|
||||
// Rendered area of object-contain media inside its container, accounting for
|
||||
// letterboxing on whichever axis has slack
|
||||
function getContainedMediaRect(
|
||||
width: number,
|
||||
height: number,
|
||||
mediaDimensions: { width: number; height: number } | null,
|
||||
): MediaRect {
|
||||
if (
|
||||
!mediaDimensions ||
|
||||
mediaDimensions.width <= 0 ||
|
||||
mediaDimensions.height <= 0
|
||||
) {
|
||||
return { x: 0, y: 0, width, height };
|
||||
}
|
||||
|
||||
const containerAspect = width / height;
|
||||
const mediaAspect = mediaDimensions.width / mediaDimensions.height;
|
||||
|
||||
if (mediaAspect < containerAspect) {
|
||||
// Portrait / tall: constrained by height, bars on left and right
|
||||
const drawWidth = height * mediaAspect;
|
||||
return { x: (width - drawWidth) / 2, y: 0, width: drawWidth, height };
|
||||
}
|
||||
|
||||
// Wide / landscape: constrained by width, bars on top and bottom
|
||||
const drawHeight = width / mediaAspect;
|
||||
return { x: 0, y: (height - drawHeight) / 2, width, height: drawHeight };
|
||||
}
|
||||
|
||||
function getPreviewForMotionRange(
|
||||
cameraPreviews: Preview[],
|
||||
@@ -132,6 +220,8 @@ type MotionPreviewClipProps = {
|
||||
fallbackFrameTimes?: number[];
|
||||
motionHeatmap?: Record<string, number> | null;
|
||||
nonMotionAlpha: number;
|
||||
cropRegion?: CropRegion;
|
||||
aspectRatio: number;
|
||||
isVisible: boolean;
|
||||
onSeek: (timestamp: number) => void;
|
||||
};
|
||||
@@ -144,6 +234,8 @@ function MotionPreviewClip({
|
||||
fallbackFrameTimes,
|
||||
motionHeatmap,
|
||||
nonMotionAlpha,
|
||||
cropRegion,
|
||||
aspectRatio,
|
||||
isVisible,
|
||||
onSeek,
|
||||
}: MotionPreviewClipProps) {
|
||||
@@ -398,34 +490,12 @@ function MotionPreviewClip({
|
||||
return;
|
||||
}
|
||||
|
||||
// Calculate the actual rendered media area (object-contain letterboxing)
|
||||
let drawX = 0;
|
||||
let drawY = 0;
|
||||
let drawWidth = width;
|
||||
let drawHeight = height;
|
||||
|
||||
if (
|
||||
mediaDimensions &&
|
||||
mediaDimensions.width > 0 &&
|
||||
mediaDimensions.height > 0
|
||||
) {
|
||||
const containerAspect = width / height;
|
||||
const mediaAspect = mediaDimensions.width / mediaDimensions.height;
|
||||
|
||||
if (mediaAspect < containerAspect) {
|
||||
// Portrait / tall: constrained by height, bars on left and right
|
||||
drawHeight = height;
|
||||
drawWidth = height * mediaAspect;
|
||||
drawX = (width - drawWidth) / 2;
|
||||
drawY = 0;
|
||||
} else {
|
||||
// Wide / landscape: constrained by width, bars on top and bottom
|
||||
drawWidth = width;
|
||||
drawHeight = width / mediaAspect;
|
||||
drawX = 0;
|
||||
drawY = (height - drawHeight) / 2;
|
||||
}
|
||||
}
|
||||
const {
|
||||
x: drawX,
|
||||
y: drawY,
|
||||
width: drawWidth,
|
||||
height: drawHeight,
|
||||
} = getContainedMediaRect(width, height, mediaDimensions);
|
||||
|
||||
const heatmapLevels = Object.values(motionHeatmap)
|
||||
.map((value) => Number(value))
|
||||
@@ -484,17 +554,53 @@ function MotionPreviewClip({
|
||||
drawDimOverlay();
|
||||
}, [drawDimOverlay]);
|
||||
|
||||
// Zoom the media (and its dim overlay) so the filtered region fills the tile
|
||||
const mediaCropStyle = useMemo(() => {
|
||||
if (!cropRegion || overlayWidth <= 0 || overlayHeight <= 0) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const mediaRect = getContainedMediaRect(
|
||||
overlayWidth,
|
||||
overlayHeight,
|
||||
mediaDimensions,
|
||||
);
|
||||
const cropWidth = cropRegion.width * mediaRect.width;
|
||||
const cropHeight = cropRegion.height * mediaRect.height;
|
||||
|
||||
if (cropWidth <= 0 || cropHeight <= 0) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const cropCenterX =
|
||||
mediaRect.x + (cropRegion.x + cropRegion.width / 2) * mediaRect.width;
|
||||
const cropCenterY =
|
||||
mediaRect.y + (cropRegion.y + cropRegion.height / 2) * mediaRect.height;
|
||||
const scale = Math.min(
|
||||
overlayWidth / cropWidth,
|
||||
overlayHeight / cropHeight,
|
||||
);
|
||||
const translateX = overlayWidth / 2 - scale * cropCenterX;
|
||||
const translateY = overlayHeight / 2 - scale * cropCenterY;
|
||||
|
||||
return {
|
||||
transform: `translate(${translateX}px, ${translateY}px) scale(${scale})`,
|
||||
transformOrigin: "0 0",
|
||||
};
|
||||
}, [cropRegion, mediaDimensions, overlayHeight, overlayWidth]);
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={overlayContainerRef}
|
||||
className="relative aspect-video size-full cursor-pointer overflow-hidden rounded-lg bg-black md:rounded-2xl"
|
||||
className="relative size-full cursor-pointer overflow-hidden rounded-lg bg-black md:rounded-2xl"
|
||||
style={{ aspectRatio }}
|
||||
onClick={() => onSeek(range.start_time)}
|
||||
>
|
||||
{showLoadingIndicator && (
|
||||
<Skeleton className="absolute inset-0 z-10 rounded-lg md:rounded-2xl" />
|
||||
)}
|
||||
{preview && isVisible ? (
|
||||
<>
|
||||
<div className="absolute inset-0" style={mediaCropStyle}>
|
||||
<video
|
||||
ref={videoRef}
|
||||
className="size-full bg-black object-contain"
|
||||
@@ -548,9 +654,9 @@ function MotionPreviewClip({
|
||||
aria-hidden="true"
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
</div>
|
||||
) : fallbackFrameSrc ? (
|
||||
<>
|
||||
<div className="absolute inset-0" style={mediaCropStyle}>
|
||||
<img
|
||||
src={fallbackFrameSrc}
|
||||
className="size-full bg-black object-contain"
|
||||
@@ -575,7 +681,7 @@ function MotionPreviewClip({
|
||||
aria-hidden="true"
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex size-full items-center justify-center text-sm text-muted-foreground">
|
||||
{t("motionPreviews.noPreview")}
|
||||
@@ -605,6 +711,7 @@ type MotionPreviewsPaneProps = {
|
||||
playbackRate: number;
|
||||
nonMotionAlpha: number;
|
||||
motionFilterCells?: Set<number>;
|
||||
cropToFilter?: boolean;
|
||||
onSeek: (timestamp: number) => void;
|
||||
};
|
||||
|
||||
@@ -617,6 +724,7 @@ export default function MotionPreviewsPane({
|
||||
playbackRate,
|
||||
nonMotionAlpha,
|
||||
motionFilterCells,
|
||||
cropToFilter = true,
|
||||
onSeek,
|
||||
}: MotionPreviewsPaneProps) {
|
||||
const { t } = useTranslation(["views/events"]);
|
||||
@@ -916,6 +1024,35 @@ export default function MotionPreviewsPane({
|
||||
});
|
||||
}, [clipData, motionFilterCells]);
|
||||
|
||||
const cropRegion = useMemo(
|
||||
() => (cropToFilter ? getCropRegionForCells(motionFilterCells) : undefined),
|
||||
[cropToFilter, motionFilterCells],
|
||||
);
|
||||
|
||||
// Every clip shares the same crop, so tiles stay uniform while matching the
|
||||
// shape of the selected region instead of letterboxing it into 16:9
|
||||
const tileAspectRatio = useMemo(() => {
|
||||
if (!cropRegion) {
|
||||
return DEFAULT_TILE_ASPECT_RATIO;
|
||||
}
|
||||
|
||||
const cameraAspect =
|
||||
camera.detect.width && camera.detect.height
|
||||
? camera.detect.width / camera.detect.height
|
||||
: DEFAULT_TILE_ASPECT_RATIO;
|
||||
|
||||
const croppedAspect = cameraAspect * (cropRegion.width / cropRegion.height);
|
||||
|
||||
if (!Number.isFinite(croppedAspect) || croppedAspect <= 0) {
|
||||
return DEFAULT_TILE_ASPECT_RATIO;
|
||||
}
|
||||
|
||||
return Math.min(
|
||||
MAX_CROP_TILE_ASPECT_RATIO,
|
||||
Math.max(MIN_CROP_TILE_ASPECT_RATIO, croppedAspect),
|
||||
);
|
||||
}, [camera.detect.height, camera.detect.width, cropRegion]);
|
||||
|
||||
const hasCurrentHourRanges = useMemo(
|
||||
() => motionRanges.some((range) => isCurrentHour(range.end_time)),
|
||||
[motionRanges],
|
||||
@@ -964,6 +1101,8 @@ export default function MotionPreviewsPane({
|
||||
fallbackFrameTimes={fallbackFrameTimes}
|
||||
motionHeatmap={motionHeatmap}
|
||||
nonMotionAlpha={nonMotionAlpha}
|
||||
cropRegion={cropRegion}
|
||||
aspectRatio={tileAspectRatio}
|
||||
isVisible={
|
||||
windowVisible &&
|
||||
(visibleClips.includes(clipId) ||
|
||||
@@ -972,7 +1111,10 @@ export default function MotionPreviewsPane({
|
||||
onSeek={onSeek}
|
||||
/>
|
||||
) : (
|
||||
<div className="aspect-video rounded-lg bg-black md:rounded-2xl" />
|
||||
<div
|
||||
className="rounded-lg bg-black md:rounded-2xl"
|
||||
style={{ aspectRatio: tileAspectRatio }}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -51,7 +51,12 @@ import { useTimelineUtils } from "@/hooks/use-timeline-utils";
|
||||
import { useCameraPreviews } from "@/hooks/use-camera-previews";
|
||||
import { getChunkedTimeDay } from "@/utils/timelineUtil";
|
||||
|
||||
import { MotionData, REVIEW_PADDING, ZoomLevel } from "@/types/review";
|
||||
import {
|
||||
MotionData,
|
||||
REVIEW_PADDING,
|
||||
ReviewSegment,
|
||||
ZoomLevel,
|
||||
} from "@/types/review";
|
||||
import {
|
||||
ASPECT_VERTICAL_LAYOUT,
|
||||
ASPECT_WIDE_LAYOUT,
|
||||
@@ -85,6 +90,7 @@ type MotionSearchViewProps = {
|
||||
};
|
||||
|
||||
const DEFAULT_EXPORT_WINDOW_SECONDS = 60;
|
||||
const NO_REVIEW_EVENTS: ReviewSegment[] = [];
|
||||
|
||||
export default function MotionSearchView({
|
||||
config,
|
||||
@@ -514,6 +520,12 @@ export default function MotionSearchView({
|
||||
: null,
|
||||
);
|
||||
|
||||
const timelineMotionEvents = useMemo(() => motionData ?? [], [motionData]);
|
||||
const timelineNoRecordings = useMemo(
|
||||
() => noRecordings ?? [],
|
||||
[noRecordings],
|
||||
);
|
||||
|
||||
const recordingParams = useMemo(
|
||||
() => ({
|
||||
before: currentTimeRange.before,
|
||||
@@ -1054,11 +1066,11 @@ export default function MotionSearchView({
|
||||
showHandlebar={true}
|
||||
handlebarTime={currentTime}
|
||||
setHandlebarTime={setCurrentTime}
|
||||
events={[]}
|
||||
motion_events={motionData ?? []}
|
||||
noRecordingRanges={noRecordings ?? []}
|
||||
events={NO_REVIEW_EVENTS}
|
||||
motion_events={timelineMotionEvents}
|
||||
noRecordingRanges={timelineNoRecordings}
|
||||
contentRef={contentRef}
|
||||
onHandlebarDraggingChange={(dragging) => setScrubbing(dragging)}
|
||||
onHandlebarDraggingChange={setScrubbing}
|
||||
showExportHandles={
|
||||
(exportMode === "timeline" || exportMode === "timeline_multi") &&
|
||||
Boolean(exportRange)
|
||||
@@ -1489,7 +1501,7 @@ export default function MotionSearchView({
|
||||
isDesktop
|
||||
? mainCameraAspect === "tall"
|
||||
? "mr-2 h-full min-h-0 min-w-0 flex-1 items-center"
|
||||
: "mr-2 h-full min-h-0 min-w-0 flex-1"
|
||||
: "mx-2 h-full min-h-0 min-w-0 flex-1"
|
||||
: mainCameraAspect === "tall"
|
||||
? "flex-1 portrait:h-[40dvh] portrait:max-h-[40dvh] portrait:flex-shrink-0 portrait:flex-grow-0 portrait:basis-auto portrait:items-center portrait:justify-center"
|
||||
: "flex-1 portrait:max-h-[40dvh] portrait:flex-shrink-0 portrait:flex-grow-0 portrait:basis-auto landscape:items-center landscape:justify-center",
|
||||
|
||||
@@ -1199,7 +1199,7 @@ function Timeline({
|
||||
motion_events={motionData ?? []}
|
||||
noRecordingRanges={noRecordings ?? []}
|
||||
contentRef={contentRef}
|
||||
onHandlebarDraggingChange={(scrubbing) => setScrubbing(scrubbing)}
|
||||
onHandlebarDraggingChange={setScrubbing}
|
||||
isZooming={isZooming}
|
||||
zoomDirection={zoomDirection}
|
||||
onZoomChange={handleZoomChange}
|
||||
|
||||
@@ -37,6 +37,9 @@ export default defineConfig({
|
||||
},
|
||||
},
|
||||
},
|
||||
esbuild: {
|
||||
keepNames: true,
|
||||
},
|
||||
build: {
|
||||
rollupOptions: {
|
||||
input: {
|
||||
|
||||
Reference in New Issue
Block a user