mirror of
https://github.com/blakeblackshear/frigate.git
synced 2026-11-03 12:20:31 +03:00
Compare commits
37
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
12042c1d73 | ||
|
|
e6fcb60efb | ||
|
|
b6acc3c6c2 | ||
|
|
c943ecfc85 | ||
|
|
2b4907a9fb | ||
|
|
dac8f8e80b | ||
|
|
bfc074c91f | ||
|
|
5902182730 | ||
|
|
d6fe217f66 | ||
|
|
2b627f12c1 | ||
|
|
e89e8ca356 | ||
|
|
63cddfa6a4 | ||
|
|
0315f89d4b | ||
|
|
302cca6791 | ||
|
|
d64dddadc3 | ||
|
|
de9bb3945a | ||
|
|
577d28beb5 | ||
|
|
ead1896c12 | ||
|
|
fb291f2eee | ||
|
|
62effbeed6 | ||
|
|
f80f92c484 | ||
|
|
e8096c271a | ||
|
|
31abe9d1af | ||
|
|
b3d598b64b | ||
|
|
ea131e1663 | ||
|
|
3d4dd3ac4b | ||
|
|
933a7f1a3f | ||
|
|
4e5e8e3c59 | ||
|
|
b3ce4486b9 | ||
|
|
06e3d0ac5d | ||
|
|
28e3e1ec74 | ||
|
|
fa07109a85 | ||
|
|
910059281f | ||
|
|
ef44c18c07 | ||
|
|
06b059c36a | ||
|
|
26d31300e6 | ||
|
|
0013555528 |
@@ -15,12 +15,24 @@ from frigate.const import (
|
||||
)
|
||||
from frigate.ffmpeg_presets import parse_preset_hardware_acceleration_encode
|
||||
from frigate.util.config import find_config_file, resolve_ffmpeg_path
|
||||
from frigate.util.services import is_restricted_go2rtc_source
|
||||
from frigate.util.services import (
|
||||
is_go2rtc_arbitrary_exec_allowed,
|
||||
is_restricted_go2rtc_source,
|
||||
)
|
||||
|
||||
sys.path.remove("/opt/frigate")
|
||||
|
||||
yaml = YAML()
|
||||
|
||||
FRIGATE_ENV_VARS = {k: v for k, v in os.environ.items() if k.startswith("FRIGATE_")}
|
||||
# read docker secret files as env vars too
|
||||
if os.path.isdir("/run/secrets"):
|
||||
for secret_file in os.listdir("/run/secrets"):
|
||||
if secret_file.startswith("FRIGATE_"):
|
||||
FRIGATE_ENV_VARS[secret_file] = (
|
||||
Path(os.path.join("/run/secrets", secret_file)).read_text().strip()
|
||||
)
|
||||
|
||||
config_file = find_config_file()
|
||||
|
||||
try:
|
||||
@@ -100,7 +112,7 @@ for name in list(go2rtc_config.get("streams", {})):
|
||||
|
||||
if isinstance(stream, str):
|
||||
try:
|
||||
formatted_stream = substitute_frigate_vars(stream)
|
||||
formatted_stream = stream.format(**FRIGATE_ENV_VARS)
|
||||
if is_restricted_go2rtc_source(formatted_stream):
|
||||
print(
|
||||
f"[ERROR] Stream '{name}' uses a restricted source (echo/expr/exec) which is disabled by default for security. "
|
||||
@@ -119,7 +131,7 @@ for name in list(go2rtc_config.get("streams", {})):
|
||||
filtered_streams = []
|
||||
for i, stream_item in enumerate(stream):
|
||||
try:
|
||||
formatted_stream = substitute_frigate_vars(stream_item)
|
||||
formatted_stream = stream_item.format(**FRIGATE_ENV_VARS)
|
||||
if is_restricted_go2rtc_source(formatted_stream):
|
||||
print(
|
||||
f"[ERROR] Stream '{name}' item {i + 1} uses a restricted source (echo/expr/exec) which is disabled by default for security. "
|
||||
@@ -143,6 +155,20 @@ for name in list(go2rtc_config.get("streams", {})):
|
||||
)
|
||||
del go2rtc_config["streams"][name]
|
||||
|
||||
elif isinstance(stream, dict):
|
||||
# The map form ({"url": ...}) lets go2rtc resolve the source
|
||||
# recursively, so it is effectively a dynamic way to generate the URL
|
||||
# for a stream. That can only be backed by an exec source, so it cannot
|
||||
# be allowed unless arbitrary exec is explicitly enabled. When it is
|
||||
# enabled, leave the map untouched for go2rtc to resolve.
|
||||
if not is_go2rtc_arbitrary_exec_allowed():
|
||||
print(
|
||||
f"[ERROR] Stream '{name}' uses a dynamic source format which is disabled by default for security. "
|
||||
f"Set GO2RTC_ALLOW_ARBITRARY_EXEC=true to enable arbitrary exec sources."
|
||||
)
|
||||
del go2rtc_config["streams"][name]
|
||||
continue
|
||||
|
||||
# add birdseye restream stream if enabled
|
||||
if config.get("birdseye", {}).get("restream", False):
|
||||
birdseye: dict[str, Any] = config.get("birdseye")
|
||||
|
||||
@@ -7,6 +7,8 @@ import ConfigTabs from "@site/src/components/ConfigTabs";
|
||||
import TabItem from "@theme/TabItem";
|
||||
import NavPath from "@site/src/components/NavPath";
|
||||
|
||||
Frigate has two kinds of masks: motion masks and object filter masks. Both are narrow tools for fine-tuning, **not for hiding an area from Frigate**. Masks should be used sparingly; in most cases where users reach for one, a [zone](zones.md) with `required_zones` is the right tool instead. See [Which tool do I need?](#which-tool-do-i-need) and [Common mistakes](#common-mistakes) below if you're new to Frigate's mask behavior.
|
||||
|
||||
## Motion masks
|
||||
|
||||
Motion masks are used to prevent unwanted types of motion from triggering detection. Try watching the Debug feed (Settings --> Debug) with `Motion Boxes` enabled to see what may be regularly detected as motion. For example, you want to mask out your timestamp, the sky, rooftops, etc. Keep in mind that this mask only prevents motion from being detected and does not prevent objects from being detected if object detection was started due to motion in unmasked areas. Motion is also used during object tracking to refine the object detection area in the next frame. _Over-masking will make it more difficult for objects to be tracked._
|
||||
@@ -21,7 +23,16 @@ Object filter masks can be used to filter out stubborn false positives in fixed
|
||||
|
||||

|
||||
|
||||
## Creating masks
|
||||
## Which tool do I need?
|
||||
|
||||
| What you're trying to do | Recommended tool | How it works |
|
||||
| ------------------------------------------------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| Don't get alerts or recordings for activity in an area (e.g., the sidewalk in front of your house) | A [zone](zones.md) combined with `review.alerts.required_zones` (and/or `review.detections.required_zones`) | Frigate keeps detecting and tracking activity in the area, but a review item is only created once the bottom-center of an object's bounding box enters a required zone. |
|
||||
| Stop a stubborn false positive at a specific fixed spot (e.g., a tree base that keeps being detected as a person) | An **object filter mask** for that object type | Any detection of that object type whose bounding-box bottom-center lands inside the mask is treated as a false positive and discarded. |
|
||||
| Ignore motion in an area that obviously isn't an object of interest (e.g., the camera timestamp, sky, flags, treetops swaying) | A **motion mask** | Motion inside the mask is ignored when deciding whether to run object detection. Objects can still be detected in a motion masked area if motion elsewhere in the frame triggers detection. |
|
||||
| Stop tracking an object type altogether on this camera (e.g., you never care about cats) | Remove the object from the camera's [`objects.track`](objects.md) list | Frigate skips this object type entirely on this camera, regardless of where it appears. |
|
||||
|
||||
## Using the mask creator
|
||||
|
||||
<ConfigTabs>
|
||||
<TabItem value="ui">
|
||||
@@ -124,3 +135,14 @@ This is what `required_zones` are for. You should define a zone (remember this i
|
||||
> Maybe my specific situation just warrants this. I've just been having a hard time understanding the relevance of this information - it seems to be that it's exactly what would be expected when "masking out" an area of ANY image.
|
||||
|
||||
That may be the case for you. Frigate will definitely work harder tracking people on the sidewalk to make sure it doesn't miss anyone who steps foot on your stoop. The trade off with the way you have it now is slower recognition of objects and potential misses. That may be acceptable based on your needs. Also, if your resolution is low enough on the detect stream, your regions may already be so big that they grab the entire object anyway.
|
||||
|
||||
## Common mistakes
|
||||
|
||||
**"I added a motion mask to ignore my driveway/sidewalk."**
|
||||
A motion mask doesn't hide an area from Frigate. Objects can still be detected and tracked inside a masked area. The mask only stops motion _in that area_ from triggering object detection. If you want activity on the sidewalk to never produce a review item, define a [zone](zones.md) over the area you DO care about (your stoop, your driveway) and add it to `review.alerts.required_zones`. Frigate will still see people on the sidewalk, but it won't create an alert until they cross into the zone.
|
||||
|
||||
**"I added an object filter mask because I don't care about cars in my yard."**
|
||||
Object filter masks are for stubborn false positives at fixed locations, not for filtering whole areas or whole object types. If you only want alerts when a car enters the driveway, use a [zone](zones.md) with `required_zones`. If you don't care about a whole object type on this camera, remove it from [`objects.track`](objects.md).
|
||||
|
||||
**"I masked everything except a thin strip on my stoop."**
|
||||
Heavy masking hurts tracking. Frigate uses motion near a tracked object's previous bounding box to decide where to look in the next frame; with most of the frame masked, an object walking from an unmasked area into a masked one effectively disappears and gets picked up as a "new" object when it reappears. For example: someone walks down your sidewalk, stops under a tree (masked area) to tie their shoe, then continues. Frigate sees that as two separate people and can create two separate review items. Because Frigate needs several consecutive frames above the confidence threshold to commit to a detection, each re-appearance can also delay or miss alerts. Use `required_zones` for "only alert me about this spot" and leave the surrounding area unmasked so tracking stays intact.
|
||||
|
||||
@@ -124,3 +124,19 @@ cameras:
|
||||
width: 1280
|
||||
height: 720
|
||||
```
|
||||
|
||||
### Why does Frigate keep creating new events for my parked car?
|
||||
|
||||
Stationary tracking is designed to _prevent_ this — a parked car should stay one tracked object and not generate new events. If you're getting repeated events for the same car, it's likely that Frigate is losing the tracked object and re-detecting it as a new one.
|
||||
|
||||
Open one of the events in Explore → **Tracking Details**. If the detection scores are low (< 70% or so), the model isn't confident the parked car is a car. This is common with the free [COCO-trained](https://cocodataset.org/#explore) object detection models on steep/top-down angles, partially occluded cars, foliage, or low-light footage. When detections fall below `min_score` for too many frames the tracker loses the object, and the next confident frame creates a brand new one.
|
||||
|
||||
What helps:
|
||||
|
||||
- **Improve the view** — even a small angle change that gets more of the car visible could lift scores enough to stabilize tracking.
|
||||
- **Use a more accurate model** — switching from `mobiledet` to `yolov9`, or stepping up to a larger variant like `yolov9-s` over `yolov9-t`, can help (at the cost of inference time, and still on the COCO dataset). The biggest gains usually come from fine-tuning a model on images from your own cameras so it learns your specific scene. [Frigate+](https://frigate.video/plus) is a paid option that does this - models are trained on security-camera footage and can be fine-tuned on images you submit from your own setup.
|
||||
- **Don't set `detect -> stationary -> max_frames` for `car`** — it artificially ends tracking and forces re-detection as a new object. See [Stationary Objects](../configuration/stationary_objects.md).
|
||||
- **Restrict alerts to the areas you care about** with `required_zones` — see [Zones](../configuration/zones.md#restricting-alerts-and-detections-to-specific-zones). Make sure those zones use the default `loitering_time: 0` unless you specifically want the review item to stay open until the car leaves.
|
||||
- **Filter impossible locations** with [object filter masks](../configuration/masks.md#object-filter-masks) if cars are being detected on rooftops, treetops, etc.
|
||||
|
||||
See [Object Filters](../configuration/object_filters.md) for more on tuning `min_score` and `threshold` — note that raising them too high will make this exact problem worse.
|
||||
|
||||
Vendored
+14
@@ -7393,6 +7393,13 @@ components:
|
||||
required:
|
||||
- value
|
||||
title: CameraSetBody
|
||||
ChaptersEnum:
|
||||
type: string
|
||||
enum:
|
||||
- none
|
||||
- recording_segments
|
||||
- review_items
|
||||
title: ChaptersEnum
|
||||
ChatCompletionRequest:
|
||||
properties:
|
||||
messages:
|
||||
@@ -8119,6 +8126,13 @@ components:
|
||||
- type: 'null'
|
||||
title: Export case ID
|
||||
description: ID of the export case to assign this export to
|
||||
chapters:
|
||||
anyOf:
|
||||
- $ref: '#/components/schemas/ChaptersEnum'
|
||||
- type: 'null'
|
||||
title: Chapter mode
|
||||
description: Optional chapter metadata to embed in the export. When
|
||||
omitted, the camera's configured export chapter mode is used.
|
||||
type: object
|
||||
title: ExportRecordingsBody
|
||||
ExportRecordingsCustomBody:
|
||||
|
||||
@@ -147,6 +147,19 @@ def go2rtc_camera_stream(request: Request, stream_name: str):
|
||||
)
|
||||
def go2rtc_add_stream(request: Request, stream_name: str, src: str = ""):
|
||||
"""Add or update a go2rtc stream configuration."""
|
||||
if src and is_restricted_go2rtc_source(src):
|
||||
logger.warning(
|
||||
"Rejected go2rtc stream '%s' with restricted source type (echo/expr/exec)",
|
||||
stream_name,
|
||||
)
|
||||
return JSONResponse(
|
||||
content={
|
||||
"success": False,
|
||||
"message": "Restricted stream source type",
|
||||
},
|
||||
status_code=400,
|
||||
)
|
||||
|
||||
try:
|
||||
params = {"name": stream_name}
|
||||
if src:
|
||||
|
||||
@@ -3,7 +3,10 @@ from typing import Optional, Union
|
||||
from pydantic import BaseModel, Field
|
||||
from pydantic.json_schema import SkipJsonSchema
|
||||
|
||||
from frigate.record.export import PlaybackSourceEnum
|
||||
from frigate.record.export import (
|
||||
ChaptersEnum,
|
||||
PlaybackSourceEnum,
|
||||
)
|
||||
|
||||
|
||||
class ExportRecordingsBody(BaseModel):
|
||||
@@ -18,6 +21,14 @@ class ExportRecordingsBody(BaseModel):
|
||||
max_length=30,
|
||||
description="ID of the export case to assign this export to",
|
||||
)
|
||||
chapters: Optional[ChaptersEnum] = Field(
|
||||
default=None,
|
||||
title="Chapter mode",
|
||||
description=(
|
||||
"Optional chapter metadata to embed in the export. When omitted, "
|
||||
"the camera's configured export chapter mode is used."
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
class ExportRecordingsCustomBody(BaseModel):
|
||||
|
||||
@@ -68,6 +68,7 @@ from frigate.jobs.export import (
|
||||
from frigate.models import Export, ExportCase, Previews, Recordings
|
||||
from frigate.record.export import (
|
||||
DEFAULT_TIME_LAPSE_FFMPEG_ARGS,
|
||||
ChaptersEnum,
|
||||
PlaybackSourceEnum,
|
||||
validate_ffmpeg_args,
|
||||
)
|
||||
@@ -128,6 +129,15 @@ def _validate_export_case(export_case_id: Optional[str]) -> Optional[JSONRespons
|
||||
def _sanitize_existing_image(
|
||||
image_path: Optional[str],
|
||||
) -> tuple[Optional[str], Optional[JSONResponse]]:
|
||||
# sanitize_filepath normalizes "\" to "/" but leaves ".." intact, so a path
|
||||
# like "clips\..\..\etc/passwd" passes the CLIPS_DIR prefix check yet still
|
||||
# escapes the directory once resolved. A valid snapshot path never uses "..".
|
||||
if image_path and ".." in image_path:
|
||||
return None, JSONResponse(
|
||||
content={"success": False, "message": "Invalid image path"},
|
||||
status_code=400,
|
||||
)
|
||||
|
||||
existing_image = sanitize_filepath(image_path) if image_path else None
|
||||
|
||||
if existing_image and not existing_image.startswith(CLIPS_DIR):
|
||||
@@ -254,6 +264,7 @@ def _build_export_job(
|
||||
ffmpeg_input_args: Optional[str] = None,
|
||||
ffmpeg_output_args: Optional[str] = None,
|
||||
cpu_fallback: bool = False,
|
||||
chapters: Optional[ChaptersEnum] = None,
|
||||
) -> ExportJob:
|
||||
return ExportJob(
|
||||
id=_generate_export_id(camera_name),
|
||||
@@ -267,6 +278,7 @@ def _build_export_job(
|
||||
ffmpeg_input_args=ffmpeg_input_args,
|
||||
ffmpeg_output_args=ffmpeg_output_args,
|
||||
cpu_fallback=cpu_fallback,
|
||||
chapters=chapters,
|
||||
)
|
||||
|
||||
|
||||
@@ -725,6 +737,9 @@ def export_recordings_batch(
|
||||
sanitized_images[index],
|
||||
PlaybackSourceEnum.recordings,
|
||||
export_case_id,
|
||||
chapters=request.app.frigate_config.cameras[
|
||||
item.camera
|
||||
].record.export.chapters,
|
||||
)
|
||||
try:
|
||||
start_export_job(request.app.frigate_config, export_job)
|
||||
@@ -803,6 +818,14 @@ def export_recording(
|
||||
|
||||
export_case_id = body.export_case_id
|
||||
|
||||
# a chapters value in the request body overrides the camera's export config
|
||||
camera_config = request.app.frigate_config.cameras[camera_name]
|
||||
chapters = (
|
||||
body.chapters
|
||||
if body.chapters is not None
|
||||
else camera_config.record.export.chapters
|
||||
)
|
||||
|
||||
# Attaching to an existing case requires admin. Single-export for
|
||||
# cameras the user can access is otherwise non-admin; we only gate
|
||||
# the case-attachment side effect.
|
||||
@@ -839,6 +862,7 @@ def export_recording(
|
||||
existing_image,
|
||||
playback_source,
|
||||
export_case_id,
|
||||
chapters=chapters,
|
||||
)
|
||||
try:
|
||||
start_export_job(request.app.frigate_config, export_job)
|
||||
|
||||
+32
-13
@@ -60,6 +60,19 @@ logger = logging.getLogger(__name__)
|
||||
router = APIRouter(tags=[Tags.media])
|
||||
|
||||
|
||||
def _resolve_cache_age(max_cache_age: int) -> int:
|
||||
"""Return max_cache_age as an int.
|
||||
|
||||
When a media handler is invoked directly by another handler instead of
|
||||
through its route, FastAPI doesn't resolve the Query() default and
|
||||
max_cache_age arrives as the Query object; fall back to its int default.
|
||||
"""
|
||||
if isinstance(max_cache_age, int):
|
||||
return max_cache_age
|
||||
|
||||
return max_cache_age.default
|
||||
|
||||
|
||||
@router.get("/{camera_name}", dependencies=[Depends(require_camera_access)])
|
||||
async def mjpeg_feed(
|
||||
request: Request,
|
||||
@@ -413,7 +426,9 @@ async def submit_recording_snapshot_to_plus(
|
||||
)
|
||||
|
||||
nd = cv2.imdecode(np.frombuffer(image_data, dtype=np.int8), cv2.IMREAD_COLOR)
|
||||
request.app.frigate_config.plus_api.upload_image(nd, camera_name)
|
||||
await asyncio.to_thread(
|
||||
request.app.frigate_config.plus_api.upload_image, nd, camera_name
|
||||
)
|
||||
|
||||
return JSONResponse(
|
||||
content={
|
||||
@@ -936,7 +951,7 @@ async def event_thumbnail(
|
||||
thumbnail_bytes,
|
||||
media_type=extension.get_mime_type(),
|
||||
headers={
|
||||
"Cache-Control": f"private, max-age={max_cache_age}"
|
||||
"Cache-Control": f"private, max-age={_resolve_cache_age(max_cache_age)}"
|
||||
if event_complete
|
||||
else "no-store",
|
||||
},
|
||||
@@ -1270,14 +1285,14 @@ async def event_preview(request: Request, event_id: str):
|
||||
end_ts = start_ts + (
|
||||
min(event.end_time - event.start_time, 20) if event.end_time else 20
|
||||
)
|
||||
return preview_gif(request, event.camera, start_ts, end_ts)
|
||||
return await preview_gif(request, event.camera, start_ts, end_ts)
|
||||
|
||||
|
||||
@router.get(
|
||||
"/{camera_name}/start/{start_ts}/end/{end_ts}/preview.gif",
|
||||
dependencies=[Depends(require_camera_access)],
|
||||
)
|
||||
def preview_gif(
|
||||
async def preview_gif(
|
||||
request: Request,
|
||||
camera_name: str,
|
||||
start_ts: float,
|
||||
@@ -1340,7 +1355,8 @@ def preview_gif(
|
||||
"-",
|
||||
]
|
||||
|
||||
process = sp.run(
|
||||
process = await asyncio.to_thread(
|
||||
sp.run,
|
||||
ffmpeg_cmd,
|
||||
capture_output=True,
|
||||
)
|
||||
@@ -1419,7 +1435,8 @@ def preview_gif(
|
||||
"-",
|
||||
]
|
||||
|
||||
process = sp.run(
|
||||
process = await asyncio.to_thread(
|
||||
sp.run,
|
||||
ffmpeg_cmd,
|
||||
input=str.encode("\n".join(selected_previews)),
|
||||
capture_output=True,
|
||||
@@ -1438,7 +1455,7 @@ def preview_gif(
|
||||
gif_bytes,
|
||||
media_type="image/gif",
|
||||
headers={
|
||||
"Cache-Control": f"private, max-age={max_cache_age}",
|
||||
"Cache-Control": f"private, max-age={_resolve_cache_age(max_cache_age)}",
|
||||
"Content-Type": "image/gif",
|
||||
},
|
||||
)
|
||||
@@ -1448,7 +1465,7 @@ def preview_gif(
|
||||
"/{camera_name}/start/{start_ts}/end/{end_ts}/preview.mp4",
|
||||
dependencies=[Depends(require_camera_access)],
|
||||
)
|
||||
def preview_mp4(
|
||||
async def preview_mp4(
|
||||
request: Request,
|
||||
camera_name: str,
|
||||
start_ts: float,
|
||||
@@ -1528,7 +1545,8 @@ def preview_mp4(
|
||||
path,
|
||||
]
|
||||
|
||||
process = sp.run(
|
||||
process = await asyncio.to_thread(
|
||||
sp.run,
|
||||
ffmpeg_cmd,
|
||||
capture_output=True,
|
||||
)
|
||||
@@ -1604,7 +1622,8 @@ def preview_mp4(
|
||||
path,
|
||||
]
|
||||
|
||||
process = sp.run(
|
||||
process = await asyncio.to_thread(
|
||||
sp.run,
|
||||
ffmpeg_cmd,
|
||||
input=str.encode("\n".join(selected_previews)),
|
||||
capture_output=True,
|
||||
@@ -1619,7 +1638,7 @@ def preview_mp4(
|
||||
|
||||
headers = {
|
||||
"Content-Description": "File Transfer",
|
||||
"Cache-Control": f"private, max-age={max_cache_age}",
|
||||
"Cache-Control": f"private, max-age={_resolve_cache_age(max_cache_age)}",
|
||||
"Content-Type": "video/mp4",
|
||||
"Content-Length": str(os.path.getsize(path)),
|
||||
# nginx: https://nginx.org/en/docs/http/ngx_http_proxy_module.html#proxy_ignore_headers
|
||||
@@ -1657,9 +1676,9 @@ async def review_preview(
|
||||
)
|
||||
|
||||
if format == "gif":
|
||||
return preview_gif(request, review.camera, start_ts, end_ts)
|
||||
return await preview_gif(request, review.camera, start_ts, end_ts)
|
||||
else:
|
||||
return preview_mp4(request, review.camera, start_ts, end_ts)
|
||||
return await preview_mp4(request, review.camera, start_ts, end_ts)
|
||||
|
||||
|
||||
@router.get(
|
||||
|
||||
+39
-7
@@ -72,11 +72,16 @@ _WS_VIEWER_TOPICS = frozenset(
|
||||
}
|
||||
)
|
||||
|
||||
# Camera-scoped command topics a camera-authorized (non-admin) user may send.
|
||||
_WS_CAMERA_COMMAND_TOPICS = frozenset({"ptz"})
|
||||
|
||||
|
||||
def _check_ws_authorization(
|
||||
topic: str,
|
||||
role_header: str | None,
|
||||
separator: str,
|
||||
roles_config: dict[str, list[str]] | None = None,
|
||||
camera_names: set[str] | None = None,
|
||||
) -> bool:
|
||||
"""Check if a WebSocket message is authorized.
|
||||
|
||||
@@ -84,6 +89,10 @@ def _check_ws_authorization(
|
||||
topic: The message topic.
|
||||
role_header: The HTTP_REMOTE_ROLE header value, or None.
|
||||
separator: The role separator character from proxy config.
|
||||
roles_config: The auth.roles mapping (role -> allowed cameras), used to
|
||||
authorize camera-scoped commands for non-admin users.
|
||||
camera_names: All configured camera names, used to resolve a role's
|
||||
allowed cameras.
|
||||
|
||||
Returns:
|
||||
True if authorized, False if blocked.
|
||||
@@ -93,16 +102,33 @@ def _check_ws_authorization(
|
||||
return False
|
||||
|
||||
# No role header: default to viewer (fail-closed)
|
||||
if role_header is None:
|
||||
return topic in _WS_VIEWER_TOPICS
|
||||
roles = [r.strip() for r in role_header.split(separator)] if role_header else []
|
||||
|
||||
# Check if any role is admin
|
||||
roles = [r.strip() for r in role_header.split(separator)]
|
||||
# Admin can send anything
|
||||
if "admin" in roles:
|
||||
return True
|
||||
|
||||
# Non-admin: only viewer topics allowed
|
||||
return topic in _WS_VIEWER_TOPICS
|
||||
# Read-only topics any authenticated user can send
|
||||
if topic in _WS_VIEWER_TOPICS:
|
||||
return True
|
||||
|
||||
# Camera-scoped command like "<camera>/ptz": allow when the user's role(s)
|
||||
# grant access to that camera.
|
||||
parts = topic.split("/")
|
||||
if (
|
||||
roles_config is not None
|
||||
and len(parts) == 2
|
||||
and parts[1] in _WS_CAMERA_COMMAND_TOPICS
|
||||
):
|
||||
allowed: set[str] = set()
|
||||
# No role header maps to the default viewer role (e.g. proxy-only setups)
|
||||
for role in roles or ["viewer"]:
|
||||
allowed.update(
|
||||
User.get_allowed_cameras(role, roles_config, camera_names or set())
|
||||
)
|
||||
return parts[0] in allowed
|
||||
|
||||
return False
|
||||
|
||||
|
||||
# ---- Outbound filtering ---------------------------------------------------
|
||||
@@ -449,6 +475,8 @@ class WebSocketClient(Communicator):
|
||||
class _WebSocketHandler(WebSocket):
|
||||
receiver = self._dispatcher
|
||||
role_separator = self.config.proxy.separator or ","
|
||||
roles_config = self.config.auth.roles
|
||||
camera_names = set(self.config.cameras.keys())
|
||||
|
||||
def received_message(self, message: WebSocket.received_message) -> None: # type: ignore[name-defined]
|
||||
try:
|
||||
@@ -470,7 +498,11 @@ class WebSocketClient(Communicator):
|
||||
self.environ.get("HTTP_REMOTE_ROLE") if self.environ else None
|
||||
)
|
||||
if self.environ is not None and not _check_ws_authorization(
|
||||
topic, role_header, self.role_separator
|
||||
topic,
|
||||
role_header,
|
||||
self.role_separator,
|
||||
self.roles_config,
|
||||
self.camera_names,
|
||||
):
|
||||
logger.warning(
|
||||
"Blocked unauthorized WebSocket message: topic=%s, role=%s",
|
||||
|
||||
@@ -9,6 +9,7 @@ from frigate.review.types import SeverityEnum
|
||||
from ..base import FrigateBaseModel
|
||||
|
||||
__all__ = [
|
||||
"ChaptersEnum",
|
||||
"RecordConfig",
|
||||
"RecordExportConfig",
|
||||
"RecordPreviewConfig",
|
||||
@@ -86,6 +87,12 @@ class RecordPreviewConfig(FrigateBaseModel):
|
||||
)
|
||||
|
||||
|
||||
class ChaptersEnum(str, Enum):
|
||||
none = "none"
|
||||
recording_segments = "recording_segments"
|
||||
review_items = "review_items"
|
||||
|
||||
|
||||
class RecordExportConfig(FrigateBaseModel):
|
||||
hwaccel_args: Union[str, list[str]] = Field(
|
||||
default="auto",
|
||||
@@ -98,6 +105,10 @@ class RecordExportConfig(FrigateBaseModel):
|
||||
title="Maximum concurrent exports",
|
||||
description="Maximum number of export jobs to process at the same time.",
|
||||
)
|
||||
chapters: ChaptersEnum = Field(
|
||||
default=ChaptersEnum.review_items,
|
||||
title="Chapter metadata to embed in exported recordings",
|
||||
)
|
||||
|
||||
|
||||
class RecordConfig(FrigateBaseModel):
|
||||
|
||||
@@ -13,6 +13,7 @@ from peewee import DoesNotExist
|
||||
|
||||
from frigate.comms.inter_process import InterProcessRequestor
|
||||
from frigate.config import FrigateConfig
|
||||
from frigate.config.camera.record import ChaptersEnum
|
||||
from frigate.const import UPDATE_JOB_STATE
|
||||
from frigate.jobs.job import Job
|
||||
from frigate.models import Export
|
||||
@@ -55,6 +56,7 @@ class ExportJob(Job):
|
||||
ffmpeg_input_args: Optional[str] = None
|
||||
ffmpeg_output_args: Optional[str] = None
|
||||
cpu_fallback: bool = False
|
||||
chapters: Optional[ChaptersEnum] = None
|
||||
current_step: str = "queued"
|
||||
progress_percent: float = 0.0
|
||||
|
||||
@@ -343,6 +345,7 @@ class ExportJobManager:
|
||||
job.ffmpeg_input_args,
|
||||
job.ffmpeg_output_args,
|
||||
job.cpu_fallback,
|
||||
job.chapters,
|
||||
on_progress=self._make_progress_callback(job),
|
||||
)
|
||||
|
||||
|
||||
+113
-5
@@ -17,6 +17,7 @@ import pytz # type: ignore[import-untyped]
|
||||
from peewee import DoesNotExist
|
||||
|
||||
from frigate.config import FfmpegConfig, FrigateConfig
|
||||
from frigate.config.camera.record import ChaptersEnum
|
||||
from frigate.const import (
|
||||
CACHE_DIR,
|
||||
CLIPS_DIR,
|
||||
@@ -217,6 +218,7 @@ class RecordingExporter(threading.Thread):
|
||||
ffmpeg_input_args: Optional[str] = None,
|
||||
ffmpeg_output_args: Optional[str] = None,
|
||||
cpu_fallback: bool = False,
|
||||
chapters: Optional[ChaptersEnum] = None,
|
||||
on_progress: Optional[Callable[[str, float], None]] = None,
|
||||
) -> None:
|
||||
super().__init__()
|
||||
@@ -232,6 +234,7 @@ class RecordingExporter(threading.Thread):
|
||||
self.ffmpeg_input_args = ffmpeg_input_args
|
||||
self.ffmpeg_output_args = ffmpeg_output_args
|
||||
self.cpu_fallback = cpu_fallback
|
||||
self.chapters = chapters
|
||||
self.on_progress = on_progress
|
||||
|
||||
# ensure export thumb dir
|
||||
@@ -509,6 +512,74 @@ class RecordingExporter(threading.Thread):
|
||||
|
||||
return meta_path
|
||||
|
||||
def _build_recording_segment_chapter_metadata_file(
|
||||
self, recordings: list
|
||||
) -> Optional[str]:
|
||||
"""Write an FFmpeg metadata file with one chapter per recording segment.
|
||||
|
||||
Each chapter's title is the segment's wallclock start time in
|
||||
strict ISO 8601 form so a viewer can map any point in the
|
||||
export's playback timeline back to real-world time without
|
||||
OCR-ing a burnt-in timestamp. Chapter offsets are computed in
|
||||
*output time*: the VOD endpoint concatenates recording clips
|
||||
back-to-back, so wall-clock gaps between recordings collapse in
|
||||
the produced video. Returns ``None`` when there are no
|
||||
recordings or every segment is empty after clipping.
|
||||
"""
|
||||
if not recordings:
|
||||
return None
|
||||
|
||||
tz_name = self.config.ui.timezone
|
||||
tz: Optional[datetime.tzinfo] = None
|
||||
if tz_name:
|
||||
try:
|
||||
tz = pytz.timezone(tz_name)
|
||||
except pytz.UnknownTimeZoneError:
|
||||
tz = None
|
||||
if tz is None:
|
||||
tz = datetime.timezone.utc
|
||||
|
||||
chapter_blocks: list[str] = []
|
||||
output_offset_ms = 0
|
||||
for rec in recordings:
|
||||
clipped_start = max(float(rec.start_time), float(self.start_time))
|
||||
clipped_end = min(float(rec.end_time), float(self.end_time))
|
||||
if clipped_end <= clipped_start:
|
||||
continue
|
||||
|
||||
duration_ms = int(round((clipped_end - clipped_start) * 1000))
|
||||
if duration_ms <= 0:
|
||||
continue
|
||||
|
||||
title = datetime.datetime.fromtimestamp(clipped_start, tz=tz).isoformat(
|
||||
timespec="seconds"
|
||||
)
|
||||
chapter_blocks.append(
|
||||
"[CHAPTER]\n"
|
||||
"TIMEBASE=1/1000\n"
|
||||
f"START={output_offset_ms}\n"
|
||||
f"END={output_offset_ms + duration_ms}\n"
|
||||
f"title={title}"
|
||||
)
|
||||
output_offset_ms += duration_ms
|
||||
|
||||
if not chapter_blocks:
|
||||
return None
|
||||
|
||||
meta_path = self._chapter_metadata_path()
|
||||
try:
|
||||
with open(meta_path, "w", encoding="utf-8") as f:
|
||||
f.write(";FFMETADATA1\n")
|
||||
f.write("\n".join(chapter_blocks))
|
||||
f.write("\n")
|
||||
except OSError:
|
||||
logger.exception(
|
||||
"Failed to write chapter metadata file for export %s", self.export_id
|
||||
)
|
||||
return None
|
||||
|
||||
return meta_path
|
||||
|
||||
def save_thumbnail(self, id: str) -> str:
|
||||
thumb_path = os.path.join(CLIPS_DIR, f"export/{id}.webp")
|
||||
|
||||
@@ -672,7 +743,18 @@ class RecordingExporter(threading.Thread):
|
||||
)
|
||||
).split(" ")
|
||||
else:
|
||||
chapters_path = self._build_chapter_metadata_file(recordings)
|
||||
# Realtime/stream-copy export. Embed chapter metadata according to
|
||||
# the camera's configured chapter mode: per-recording-segment
|
||||
# timestamps or per-review-item titles.
|
||||
if self.chapters == ChaptersEnum.recording_segments:
|
||||
chapters_path = self._build_recording_segment_chapter_metadata_file(
|
||||
recordings
|
||||
)
|
||||
elif self.chapters == ChaptersEnum.review_items:
|
||||
chapters_path = self._build_chapter_metadata_file(recordings)
|
||||
else:
|
||||
chapters_path = None
|
||||
|
||||
chapter_args = (
|
||||
f" -i {chapters_path} -map 0 -dn -map_metadata 1"
|
||||
if chapters_path
|
||||
@@ -684,7 +766,19 @@ class RecordingExporter(threading.Thread):
|
||||
|
||||
# add metadata
|
||||
title = f"Frigate Recording for {self.camera}, {self.get_datetime_from_timestamp(self.start_time)} - {self.get_datetime_from_timestamp(self.end_time)}"
|
||||
ffmpeg_cmd.extend(["-metadata", f"title={title}"])
|
||||
creation_time = datetime.datetime.fromtimestamp(
|
||||
self.start_time, tz=datetime.timezone.utc
|
||||
).strftime("%Y-%m-%dT%H:%M:%S.%fZ")
|
||||
ffmpeg_cmd.extend(
|
||||
[
|
||||
"-metadata",
|
||||
f"title={title}",
|
||||
"-metadata",
|
||||
f"creation_time={creation_time}",
|
||||
"-metadata",
|
||||
f"comment=Camera: {self.camera}",
|
||||
]
|
||||
)
|
||||
|
||||
ffmpeg_cmd.append(video_path)
|
||||
|
||||
@@ -770,18 +864,32 @@ class RecordingExporter(threading.Thread):
|
||||
self.config.ffmpeg.ffmpeg_path,
|
||||
hwaccel_args,
|
||||
f"{self.ffmpeg_input_args} {TIMELAPSE_DATA_INPUT_ARGS} {ffmpeg_input}".strip(),
|
||||
f"{self.ffmpeg_output_args} -movflags +faststart {video_path}".strip(),
|
||||
f"{self.ffmpeg_output_args} -movflags +faststart".strip(),
|
||||
EncodeTypeEnum.timelapse,
|
||||
)
|
||||
).split(" ")
|
||||
else:
|
||||
ffmpeg_cmd = (
|
||||
f"{self.config.ffmpeg.ffmpeg_path} -hide_banner {ffmpeg_input} {codec} -movflags +faststart {video_path}"
|
||||
f"{self.config.ffmpeg.ffmpeg_path} -hide_banner {ffmpeg_input} {codec} -movflags +faststart"
|
||||
).split(" ")
|
||||
|
||||
# add metadata
|
||||
title = f"Frigate Preview for {self.camera}, {self.get_datetime_from_timestamp(self.start_time)} - {self.get_datetime_from_timestamp(self.end_time)}"
|
||||
ffmpeg_cmd.extend(["-metadata", f"title={title}"])
|
||||
creation_time = datetime.datetime.fromtimestamp(
|
||||
self.start_time, tz=datetime.timezone.utc
|
||||
).strftime("%Y-%m-%dT%H:%M:%S.%fZ")
|
||||
ffmpeg_cmd.extend(
|
||||
[
|
||||
"-metadata",
|
||||
f"title={title}",
|
||||
"-metadata",
|
||||
f"creation_time={creation_time}",
|
||||
"-metadata",
|
||||
f"comment=Camera: {self.camera}",
|
||||
]
|
||||
)
|
||||
|
||||
ffmpeg_cmd.append(video_path)
|
||||
|
||||
return ffmpeg_cmd, playlist_lines
|
||||
|
||||
|
||||
@@ -650,6 +650,8 @@ class RecordingMaintainer(threading.Thread):
|
||||
"copy",
|
||||
"-movflags",
|
||||
"+faststart",
|
||||
"-metadata",
|
||||
f"creation_time={start_time.strftime('%Y-%m-%dT%H:%M:%S.%fZ')}",
|
||||
file_path,
|
||||
stderr=asyncio.subprocess.PIPE,
|
||||
stdout=asyncio.subprocess.DEVNULL,
|
||||
|
||||
@@ -11,6 +11,16 @@ class TestCheckWsAuthorization(unittest.TestCase):
|
||||
|
||||
DEFAULT_SEPARATOR = ","
|
||||
|
||||
# admin/viewer are reserved and always map to all cameras (empty list);
|
||||
# custom roles map to a specific set of cameras.
|
||||
ROLES_CONFIG = {
|
||||
"admin": [],
|
||||
"viewer": [],
|
||||
"yard": ["front_door", "backyard"],
|
||||
"garage_only": ["garage"],
|
||||
}
|
||||
CAMERA_NAMES = {"front_door", "backyard", "garage"}
|
||||
|
||||
# --- IPC topic blocking (unconditional, regardless of role) ---
|
||||
|
||||
def test_ipc_topic_blocked_for_admin(self):
|
||||
@@ -161,6 +171,124 @@ class TestCheckWsAuthorization(unittest.TestCase):
|
||||
_check_ws_authorization("onConnect", None, self.DEFAULT_SEPARATOR)
|
||||
)
|
||||
|
||||
# --- Camera-scoped PTZ access (non-admin with camera access) ---
|
||||
|
||||
def test_viewer_can_ptz_camera_with_access(self):
|
||||
# viewer maps to all cameras, so PTZ is allowed
|
||||
self.assertTrue(
|
||||
_check_ws_authorization(
|
||||
"front_door/ptz",
|
||||
"viewer",
|
||||
self.DEFAULT_SEPARATOR,
|
||||
self.ROLES_CONFIG,
|
||||
self.CAMERA_NAMES,
|
||||
)
|
||||
)
|
||||
|
||||
def test_custom_role_can_ptz_assigned_camera(self):
|
||||
self.assertTrue(
|
||||
_check_ws_authorization(
|
||||
"front_door/ptz",
|
||||
"yard",
|
||||
self.DEFAULT_SEPARATOR,
|
||||
self.ROLES_CONFIG,
|
||||
self.CAMERA_NAMES,
|
||||
)
|
||||
)
|
||||
|
||||
def test_custom_role_blocked_from_ptz_unassigned_camera(self):
|
||||
self.assertFalse(
|
||||
_check_ws_authorization(
|
||||
"garage/ptz",
|
||||
"yard",
|
||||
self.DEFAULT_SEPARATOR,
|
||||
self.ROLES_CONFIG,
|
||||
self.CAMERA_NAMES,
|
||||
)
|
||||
)
|
||||
|
||||
def test_multiple_roles_union_camera_access_for_ptz(self):
|
||||
# "yard" covers front_door/backyard, "garage_only" covers garage
|
||||
self.assertTrue(
|
||||
_check_ws_authorization(
|
||||
"garage/ptz",
|
||||
"yard,garage_only",
|
||||
self.DEFAULT_SEPARATOR,
|
||||
self.ROLES_CONFIG,
|
||||
self.CAMERA_NAMES,
|
||||
)
|
||||
)
|
||||
|
||||
def test_unknown_role_blocked_from_ptz(self):
|
||||
self.assertFalse(
|
||||
_check_ws_authorization(
|
||||
"front_door/ptz",
|
||||
"nonexistent",
|
||||
self.DEFAULT_SEPARATOR,
|
||||
self.ROLES_CONFIG,
|
||||
self.CAMERA_NAMES,
|
||||
)
|
||||
)
|
||||
|
||||
def test_no_role_header_treated_as_viewer_for_ptz(self):
|
||||
# proxy-only / auth-disabled setups default to the viewer role
|
||||
self.assertTrue(
|
||||
_check_ws_authorization(
|
||||
"front_door/ptz",
|
||||
None,
|
||||
self.DEFAULT_SEPARATOR,
|
||||
self.ROLES_CONFIG,
|
||||
self.CAMERA_NAMES,
|
||||
)
|
||||
)
|
||||
|
||||
def test_camera_access_does_not_grant_set_commands(self):
|
||||
# camera access enables PTZ only, not config-changing "set" commands
|
||||
self.assertFalse(
|
||||
_check_ws_authorization(
|
||||
"front_door/detect/set",
|
||||
"yard",
|
||||
self.DEFAULT_SEPARATOR,
|
||||
self.ROLES_CONFIG,
|
||||
self.CAMERA_NAMES,
|
||||
)
|
||||
)
|
||||
|
||||
def test_ptz_autotracker_stays_admin_only(self):
|
||||
# ptz_autotracker is a config toggle, not a live-view action
|
||||
self.assertFalse(
|
||||
_check_ws_authorization(
|
||||
"front_door/ptz_autotracker/set",
|
||||
"viewer",
|
||||
self.DEFAULT_SEPARATOR,
|
||||
self.ROLES_CONFIG,
|
||||
self.CAMERA_NAMES,
|
||||
)
|
||||
)
|
||||
|
||||
def test_admin_can_ptz_any_camera_with_config(self):
|
||||
self.assertTrue(
|
||||
_check_ws_authorization(
|
||||
"garage/ptz",
|
||||
"admin",
|
||||
self.DEFAULT_SEPARATOR,
|
||||
self.ROLES_CONFIG,
|
||||
self.CAMERA_NAMES,
|
||||
)
|
||||
)
|
||||
|
||||
def test_ipc_topic_still_blocked_with_camera_access(self):
|
||||
# IPC topics are blocked unconditionally, even with camera access
|
||||
self.assertFalse(
|
||||
_check_ws_authorization(
|
||||
UPDATE_CAMERA_ACTIVITY,
|
||||
"viewer",
|
||||
self.DEFAULT_SEPARATOR,
|
||||
self.ROLES_CONFIG,
|
||||
self.CAMERA_NAMES,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
||||
@@ -790,7 +790,7 @@ def get_hailo_temps() -> dict[str, float]:
|
||||
return temps
|
||||
|
||||
|
||||
def _go2rtc_arbitrary_exec_allowed() -> bool:
|
||||
def is_go2rtc_arbitrary_exec_allowed() -> bool:
|
||||
"""Read the GO2RTC_ALLOW_ARBITRARY_EXEC override from env, docker
|
||||
secrets, or the Home Assistant add-on options file."""
|
||||
raw: Optional[str] = None
|
||||
@@ -822,7 +822,7 @@ def is_restricted_go2rtc_source(stream_source: str) -> bool:
|
||||
and the GO2RTC_ALLOW_ARBITRARY_EXEC override is not set."""
|
||||
if not stream_source.strip().startswith(("echo:", "expr:", "exec:")):
|
||||
return False
|
||||
return not _go2rtc_arbitrary_exec_allowed()
|
||||
return not is_go2rtc_arbitrary_exec_allowed()
|
||||
|
||||
|
||||
def ffprobe_stream(ffmpeg, path: str, detailed: bool = False) -> sp.CompletedProcess:
|
||||
|
||||
@@ -149,5 +149,17 @@
|
||||
"cluck": "قرقرة",
|
||||
"cock_a_doodle_doo": "كوكو-كو-كوووووو",
|
||||
"turkey": "ديك رومى",
|
||||
"gobble": "كركرة"
|
||||
"gobble": "كركرة",
|
||||
"tearing": "يمزق",
|
||||
"ping": "طنّة",
|
||||
"clang": "صوت رنين",
|
||||
"squeal": "يصرخ",
|
||||
"creak": "صرير",
|
||||
"sizzle": "صوت الأزيز",
|
||||
"clicking": "النقر",
|
||||
"clickety_clack": "طقطقة",
|
||||
"rumble": "الحلبة",
|
||||
"skateboard": "لوح تزلج",
|
||||
"echo": "صدى الصوت",
|
||||
"noise": "ازعاج"
|
||||
}
|
||||
|
||||
@@ -22,5 +22,6 @@
|
||||
"mouse": "فأر",
|
||||
"keyboard": "لوحة المفاتيح",
|
||||
"goat": "معزة",
|
||||
"sheep": "غنم"
|
||||
"sheep": "غنم",
|
||||
"skateboard": "لوح تزلج"
|
||||
}
|
||||
|
||||
@@ -24,6 +24,9 @@
|
||||
},
|
||||
"state": {
|
||||
"submitted": "Enviat"
|
||||
},
|
||||
"toast": {
|
||||
"error": "No s'ha pogut enviar a Frigate+. Si us plau, comproveu la vostra connexió de xarxa i torneu-ho a provar."
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -180,7 +180,7 @@
|
||||
"rats": "Rotter",
|
||||
"patter": "Dråbelyd",
|
||||
"insect": "Insekt",
|
||||
"cricket": "Cricket",
|
||||
"cricket": "Fårekylling",
|
||||
"guitar": "Guitar",
|
||||
"electric_guitar": "Elektrisk Guitar",
|
||||
"bass_guitar": "Basguitar",
|
||||
@@ -195,5 +195,16 @@
|
||||
"rimshot": "Kantslag",
|
||||
"drum_roll": "Trommehvirvel",
|
||||
"bass_drum": "Stortromme",
|
||||
"techno": "Techno"
|
||||
"techno": "Techno",
|
||||
"mosquito": "Myg",
|
||||
"fly": "Flue",
|
||||
"buzz": "Summen",
|
||||
"frog": "Frø",
|
||||
"croak": "Kvæk",
|
||||
"snake": "Slange",
|
||||
"rattle": "Klapren",
|
||||
"whale_vocalization": "Hvallyde",
|
||||
"music": "Musik",
|
||||
"musical_instrument": "Musikinstrument",
|
||||
"plucked_string_instrument": "Strengeinstrument"
|
||||
}
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
"am": "am",
|
||||
"year_one": "{{time}} år",
|
||||
"year_other": "{{time}} år",
|
||||
"mo": "{{time}}må",
|
||||
"mo": "{{time}}md.",
|
||||
"month_one": "{{time}} måned",
|
||||
"month_other": "{{time}} måneder",
|
||||
"d": "{{time}}d",
|
||||
@@ -121,18 +121,18 @@
|
||||
"back": "Tilbage",
|
||||
"history": "Historik",
|
||||
"fullscreen": "Fuldskærm",
|
||||
"exitFullscreen": "Afslut Fludskærm",
|
||||
"pictureInPicture": "Billede i Billede",
|
||||
"twoWayTalk": "2 vejs samtale",
|
||||
"cameraAudio": "Kamera Lyd",
|
||||
"on": "ON",
|
||||
"off": "OFF",
|
||||
"exitFullscreen": "Afslut Fuldskærm",
|
||||
"pictureInPicture": "Billede i billede",
|
||||
"twoWayTalk": "Samtale",
|
||||
"cameraAudio": "Kameralyd",
|
||||
"on": "Til",
|
||||
"off": "Fra",
|
||||
"edit": "Rediger",
|
||||
"copyCoordinates": "Kopier koordinater",
|
||||
"delete": "Slet",
|
||||
"yes": "Ja",
|
||||
"no": "Nej",
|
||||
"download": "Download",
|
||||
"download": "Hent",
|
||||
"info": "Info",
|
||||
"suspended": "Sat på pause",
|
||||
"unsuspended": "Genoptag",
|
||||
@@ -141,7 +141,19 @@
|
||||
"export": "Eksporter",
|
||||
"deleteNow": "Slet nu",
|
||||
"next": "Næste",
|
||||
"continue": "Fortsæt"
|
||||
"continue": "Fortsæt",
|
||||
"add": "Tilføj",
|
||||
"applying": "Anvender…",
|
||||
"undo": "Annuler",
|
||||
"copiedToClipboard": "Kopieret til udklipsholder",
|
||||
"modified": "Ændret",
|
||||
"overridden": "Overskrevet",
|
||||
"resetToGlobal": "Gendan til global",
|
||||
"resetToDefault": "Gendan standard",
|
||||
"saveAll": "Gem alle",
|
||||
"savingAll": "Gemmer alle…",
|
||||
"undoAll": "Fortryd alle",
|
||||
"retry": "Prøv igen"
|
||||
},
|
||||
"menu": {
|
||||
"system": "System",
|
||||
@@ -194,7 +206,9 @@
|
||||
"gl": "Galego (Galisisk)",
|
||||
"id": "Bahasa Indonesia (Indonesisk)",
|
||||
"ur": "اردو (Urdu)",
|
||||
"hr": "Hrvatski (Kroatisk)"
|
||||
"hr": "Hrvatski (Kroatisk)",
|
||||
"zhHant": "繁體中文 (Traditionel Kinesisk)",
|
||||
"bs": "Bosanski (Bosnisk)"
|
||||
},
|
||||
"appearance": "Udseende",
|
||||
"darkMode": {
|
||||
@@ -243,7 +257,11 @@
|
||||
"logout": "Log ud",
|
||||
"setPassword": "Vælg kodeord"
|
||||
},
|
||||
"classification": "Kategorisering"
|
||||
"classification": "Kategorisering",
|
||||
"profiles": "Profiler",
|
||||
"actions": "Handlinger",
|
||||
"features": "Funktioner",
|
||||
"chat": "Chat"
|
||||
},
|
||||
"toast": {
|
||||
"copyUrlToClipboard": "Kopieret URL til udklipsholder.",
|
||||
@@ -252,13 +270,14 @@
|
||||
"error": {
|
||||
"title": "Ændringer kunne ikke gemmes: {{errorMessage}}",
|
||||
"noMessage": "Kunne ikke gemme konfigurationsændringer"
|
||||
}
|
||||
},
|
||||
"success": "Gemte ændringerne i konfigurationen."
|
||||
}
|
||||
},
|
||||
"role": {
|
||||
"title": "Rolle",
|
||||
"admin": "Admin",
|
||||
"viewer": "Viewer",
|
||||
"viewer": "Seer",
|
||||
"desc": "Admins har fuld adgang til Frigate UI. Viewers er begrænset til at se kameraer, gennemse items, og historik i UI."
|
||||
},
|
||||
"pagination": {
|
||||
@@ -296,5 +315,10 @@
|
||||
},
|
||||
"information": {
|
||||
"pixels": "{{area}}px"
|
||||
},
|
||||
"no_items": "Intet fundet",
|
||||
"validation_errors": "Valideringsfejl",
|
||||
"credentialField": {
|
||||
"savedPlaceholder": "Gemt - efterlad blank for at bevare nuværende"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,7 +31,7 @@
|
||||
"setting": {
|
||||
"label": "Kamera Streaming Indstillinger",
|
||||
"title": "{{cameraName}} Streaming Indstillinger",
|
||||
"desc": "Skift de live streaming muligheder for denne kameragruppes dashboard. <em> Disse indstillinger er enheds- og browserspecifikke.</em>",
|
||||
"desc": "Skift indstillingerne for direkte visning af denne kameragruppes dashboard. <em> Disse indstillinger er enheds- og browserspecifikke.</em>",
|
||||
"audioIsAvailable": "Lyd er tilgængelig for denne stream",
|
||||
"audioIsUnavailable": "Lyd er ikke tilgængelig for denne strøm",
|
||||
"audio": {
|
||||
@@ -67,7 +67,10 @@
|
||||
"desc": "Aktivér kun denne mulighed, hvis kameraets live stream viser farve artefakter og har en diagonal linje på højre side af billedet."
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"showAll": "Vis alle kameragrupper",
|
||||
"showLess": "Vis mindre",
|
||||
"editGroups": "Rediger kameragrupper"
|
||||
},
|
||||
"debug": {
|
||||
"options": {
|
||||
@@ -81,6 +84,7 @@
|
||||
"zones": "Zoner",
|
||||
"mask": "Maske",
|
||||
"motion": "Bevægelse",
|
||||
"regions": "Regioner"
|
||||
"regions": "Regioner",
|
||||
"paths": "Stier"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"restart": {
|
||||
"title": "Er du sikker på at du vil genstarte Frigate?",
|
||||
"title": "Er du sikker på, at du vil genstarte Frigate?",
|
||||
"button": "Genstart",
|
||||
"restarting": {
|
||||
"title": "Frigate genstarter",
|
||||
@@ -21,8 +21,46 @@
|
||||
"ask_a": "Er dette objekt et <code>{{label}}</code>?",
|
||||
"ask_an": "Er dette objekt en <code>{{label}}</code>?",
|
||||
"ask_full": "Er dette objekt en <code>{{untranslatedLabel}}</code> ({{translatedLabel}})?"
|
||||
},
|
||||
"state": {
|
||||
"submitted": "Indsendt"
|
||||
},
|
||||
"toast": {
|
||||
"error": "Fejl under indsendelse til Frigate+. Kontroller venligst din netværksforbindelse og prøv igen."
|
||||
}
|
||||
}
|
||||
},
|
||||
"video": {
|
||||
"viewInHistory": "Se i historik"
|
||||
}
|
||||
},
|
||||
"export": {
|
||||
"time": {
|
||||
"fromTimeline": "Vælg fra tidslinje",
|
||||
"lastHour_one": "Sidste time",
|
||||
"lastHour_other": "Sidste {{count}} timer",
|
||||
"custom": "Tidsinterval",
|
||||
"start": {
|
||||
"title": "Starttidspunkt",
|
||||
"label": "Vælg starttidspunkt"
|
||||
},
|
||||
"end": {
|
||||
"title": "Sluttidspunkt",
|
||||
"label": "Vælg sluttidspunkt"
|
||||
}
|
||||
},
|
||||
"name": {
|
||||
"placeholder": "Navngiv eksport"
|
||||
},
|
||||
"case": {
|
||||
"newCaseOption": "Opret ny sag",
|
||||
"newCaseNamePlaceholder": "Nyt sagsnavn",
|
||||
"newCaseDescriptionPlaceholder": "Sagsbeskrivelse",
|
||||
"label": "Sag",
|
||||
"nonAdminHelp": "En ny sag oprettes til disse eksporter.",
|
||||
"placeholder": "Vælg en sag"
|
||||
},
|
||||
"select": "Vælg",
|
||||
"export": "Eksporter"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
"notifications": "Notifikations indstillinger - Frigate"
|
||||
},
|
||||
"menu": {
|
||||
"ui": "Brugergrænseflade"
|
||||
"ui": "Brugergrænseflade",
|
||||
"profiles": "Profiler"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -493,6 +493,9 @@
|
||||
"max_concurrent": {
|
||||
"label": "Maximum concurrent exports",
|
||||
"description": "Maximum number of export jobs to process at the same time."
|
||||
},
|
||||
"chapters": {
|
||||
"label": "Chapter metadata to embed in exported recordings"
|
||||
}
|
||||
},
|
||||
"preview": {
|
||||
|
||||
@@ -1035,6 +1035,9 @@
|
||||
"max_concurrent": {
|
||||
"label": "Maximum concurrent exports",
|
||||
"description": "Maximum number of export jobs to process at the same time."
|
||||
},
|
||||
"chapters": {
|
||||
"label": "Chapter metadata to embed in exported recordings"
|
||||
}
|
||||
},
|
||||
"preview": {
|
||||
|
||||
@@ -427,5 +427,11 @@
|
||||
"shatter": "Összetörés",
|
||||
"field_recording": "Helyszíni felvétel",
|
||||
"noise": "Zaj",
|
||||
"liquid": "Folyadék"
|
||||
"liquid": "Folyadék",
|
||||
"splash": "Csobbanás",
|
||||
"slosh": "Löttyenés",
|
||||
"trickle": "Csörgedezés",
|
||||
"gush": "Folyás",
|
||||
"stir": "Kavarás",
|
||||
"thump": "Puffanás"
|
||||
}
|
||||
|
||||
@@ -14,5 +14,6 @@
|
||||
"attachment_chip_remove": "Melléklet eltávolítása",
|
||||
"open_in_explore": "Megnyitás Böngészőben",
|
||||
"attachment_picker_paste_label": "Vagy illeszd be az esemény ID-t",
|
||||
"attachment_picker_attach": "Melléklet"
|
||||
"attachment_picker_attach": "Melléklet",
|
||||
"arguments": "Argumentumok:"
|
||||
}
|
||||
|
||||
@@ -497,5 +497,6 @@
|
||||
"echo": "Eco",
|
||||
"mains_hum": "Zumbido Elétrico",
|
||||
"sidetone": "Retorno de Voz",
|
||||
"throbbing": "Latejante"
|
||||
"throbbing": "Latejante",
|
||||
"sodeling": "Soldagem"
|
||||
}
|
||||
|
||||
@@ -210,7 +210,8 @@
|
||||
"id": "Bahasa Indonesia (Indonésio)",
|
||||
"ur": "اردو (Urdu)",
|
||||
"hr": "Hrvatski (Croata)",
|
||||
"bs": "Bosanski (Bósnio)"
|
||||
"bs": "Bosanski (Bósnio)",
|
||||
"zhHant": "Chinês Tradicional"
|
||||
},
|
||||
"systemLogs": "Logs de sistema",
|
||||
"settings": "Configurações",
|
||||
|
||||
@@ -68,7 +68,10 @@
|
||||
}
|
||||
},
|
||||
"birdseye": "Visão Panorâmica"
|
||||
}
|
||||
},
|
||||
"showAll": "Exibir todos os grupos de câmeras",
|
||||
"showLess": "Mostrar menos",
|
||||
"editGroups": "Editar grupos de câmeras"
|
||||
},
|
||||
"debug": {
|
||||
"options": {
|
||||
|
||||
@@ -24,6 +24,9 @@
|
||||
},
|
||||
"state": {
|
||||
"submitted": "Enviado"
|
||||
},
|
||||
"toast": {
|
||||
"error": "Falha ao enviar para o Frigate+. Verifique sua conexão de rede e tente novamente."
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -56,15 +59,24 @@
|
||||
"toast": {
|
||||
"success": "Exportação iniciada com sucesso. Veja o arquivo na tela exportar.",
|
||||
"error": {
|
||||
"failed": "Falha em iniciar exportação: {{error}}",
|
||||
"failed": "Falha ao iniciar exportação: {{error}}",
|
||||
"endTimeMustAfterStartTime": "Tempo de finalização deve ser após tempo de início",
|
||||
"noVaildTimeSelected": "Nenhuma faixa de tempo válida selecionada"
|
||||
},
|
||||
"view": "Ver"
|
||||
"view": "Ver",
|
||||
"batchQueuedSuccess_one": "1 exportação na fila. Abrindo o caso agora.",
|
||||
"batchQueuedSuccess_many": "{{count}} exportações colocadas na fila. Abrindo o caso agora.",
|
||||
"batchQueuedSuccess_other": "",
|
||||
"batchQueuedPartial": "{{successful}} de {{total}} exportações na fila. Câmeras com falha: {{failedCameras}}",
|
||||
"batchQueueFailed": "Falha ao enfileirar {{total}} exportações. Câmeras com falha: {{failedCameras}}",
|
||||
"batchPartial": "Iniciadas {{successful}} de {{total}} exportações. Câmeras com falha: {{failedCameras}}",
|
||||
"batchFailed": "Falha ao iniciar {{total}} exportações. Câmeras com falha: {{failedCameras}}"
|
||||
},
|
||||
"fromTimeline": {
|
||||
"saveExport": "Salvar Exportação",
|
||||
"previewExport": "Pré-Visualizar Exportação"
|
||||
"previewExport": "Pré-Visualizar Exportação",
|
||||
"queueingExport": "Enfileirando exportação...",
|
||||
"useThisRange": "Use esta faixa"
|
||||
},
|
||||
"case": {
|
||||
"label": "Caso",
|
||||
@@ -94,7 +106,14 @@
|
||||
"queueingButton": "Exportações na fila...",
|
||||
"exportButton_one": "Exportar câmera",
|
||||
"exportButton_many": "Exportar {{count}} câmeras",
|
||||
"exportButton_other": "Exportar {{count}} câmeras"
|
||||
"exportButton_other": "Exportar {{count}} câmeras",
|
||||
"searchOrSelectGroup": "Procurar, ou selecionar um grupo de câmeras...",
|
||||
"selectAll": "Selecionar todas as câmeras",
|
||||
"clearSelection": "Limpar seleção",
|
||||
"selectWithActivity": "Câmeras com rastreamento de objetos",
|
||||
"selectGroup": "Selecione grupo",
|
||||
"noMatchingCameras": "Nenhuma câmera corresponde à sua pesquisa",
|
||||
"selectedCount": "{{selected}} / {{total}} selecionados"
|
||||
},
|
||||
"multi": {
|
||||
"title_one": "Exportar análise",
|
||||
@@ -106,7 +125,12 @@
|
||||
"exportButton_one": "Exportar análise",
|
||||
"exportButton_many": "Exportar {{count}} análises",
|
||||
"exportButton_other": "Exportar {{count}} análises",
|
||||
"exportingButton": "Exportando..."
|
||||
"exportingButton": "Exportando...",
|
||||
"toast": {
|
||||
"started_one": "Uma exportação iniciada. Abrindo o caso agora.",
|
||||
"started_many": "Iniciadas {{count}} exportações. Abrindo o caso agora.",
|
||||
"started_other": ""
|
||||
}
|
||||
}
|
||||
},
|
||||
"streaming": {
|
||||
@@ -154,6 +178,14 @@
|
||||
"export": "Exportar",
|
||||
"deleteNow": "Deletar Agora",
|
||||
"markAsUnreviewed": "Marcar como não revisado"
|
||||
},
|
||||
"shareTimestamp": {
|
||||
"label": "Compartilhar marca de tempo",
|
||||
"title": "Compartilhar marca de tempo",
|
||||
"description": "Compartilhe uma URL com marca de tempo da posição atual da reprodução ou escolha uma marca de tempo personalizada. Observe que esta não é uma URL de compartilhamento público e só pode ser acessada por usuários que tenham acesso ao Frigate e a esta câmera.",
|
||||
"custom": "Marcação de data/hora personalizado",
|
||||
"button": "Compartilhar URL com marcação de tempo",
|
||||
"shareTitle": "Marcação de data/hora da análise do Frigate: {{camera}}"
|
||||
}
|
||||
},
|
||||
"imagePicker": {
|
||||
|
||||
@@ -4,7 +4,8 @@
|
||||
"noPreviewFoundFor": "Nenhuma Pré-Visualização Encontrada em {{cameraName}}",
|
||||
"submitFrigatePlus": {
|
||||
"title": "Enviar esse frame para Frigate+?",
|
||||
"submit": "Enviar"
|
||||
"submit": "Enviar",
|
||||
"previewError": "Não foi possível carregar a prévia do instantâneo. A gravação pode não estar disponível no momento."
|
||||
},
|
||||
"livePlayerRequiredIOSVersion": "iOS 17.1 ou superior é necessário para esse tipo de transmissão ao vivo.",
|
||||
"streamOffline": {
|
||||
@@ -47,5 +48,6 @@
|
||||
"error": {
|
||||
"submitFrigatePlusFailed": "Falha em submeter quadro ao Frigate+"
|
||||
}
|
||||
}
|
||||
},
|
||||
"cameraOff": "A câmera está desligada"
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
"description": "Habilitado"
|
||||
},
|
||||
"audio": {
|
||||
"label": "Eventos de áudio",
|
||||
"label": "Detecção de áudio",
|
||||
"description": "Configurações para detecção de eventos baseados em áudio para esta câmera.",
|
||||
"enabled": {
|
||||
"label": "Habilitar detecção de áudio",
|
||||
@@ -28,11 +28,15 @@
|
||||
},
|
||||
"listen": {
|
||||
"label": "Tipos de escuta",
|
||||
"description": "Lista de tipos de eventos de áudio a serem detectados (por exemplo: latido, alarme de incêndio, grito, fala, berro)."
|
||||
"description": "Lista de tipos de eventos de áudio a serem detectados (por exemplo: latido, alarme de incêndio, fala, grito)."
|
||||
},
|
||||
"filters": {
|
||||
"label": "Filtros de áudio",
|
||||
"description": "Configurações de filtro por tipo de áudio, como limites de confiança, usadas para reduzir falsos positivos."
|
||||
"description": "Configurações de filtro por tipo de áudio, como limites de confiança, usadas para reduzir falsos positivos.",
|
||||
"threshold": {
|
||||
"label": "Confiança mínima do áudio",
|
||||
"description": "Limite mínimo de confiança para que o evento de áudio seja contabilizado."
|
||||
}
|
||||
},
|
||||
"enabled_in_config": {
|
||||
"label": "Estado de áudio original",
|
||||
@@ -47,11 +51,122 @@
|
||||
"audio_transcription": {
|
||||
"label": "Transcrição de áudio",
|
||||
"enabled": {
|
||||
"label": "Habilitar transcrição"
|
||||
"label": "Habilitar transcrição",
|
||||
"description": "Ative ou desative a transcrição de eventos de áudio acionada manualmente."
|
||||
},
|
||||
"live_enabled": {
|
||||
"label": "Transcrição em tempo real"
|
||||
"label": "Transcrição em tempo real",
|
||||
"description": "Ative a transcrição ao vivo por streaming para o áudio à medida que ele é recebido."
|
||||
},
|
||||
"description": "Configurações de transcrição de áudio e voz ao vivo para eventos e legendas em tempo real."
|
||||
"description": "Configurações de transcrição de áudio e voz ao vivo para eventos e legendas em tempo real.",
|
||||
"enabled_in_config": {
|
||||
"label": "Estado original da transcrição"
|
||||
}
|
||||
},
|
||||
"detect": {
|
||||
"enabled": {
|
||||
"label": "Ativar detecção de objetos",
|
||||
"description": "Ative ou desative a detecção de objetos para esta câmera."
|
||||
},
|
||||
"height": {
|
||||
"label": "Detectar altura",
|
||||
"description": "Altura (em pixels) dos quadros usados para o fluxo de detecção; deixe em branco para usar a resolução nativa do fluxo."
|
||||
},
|
||||
"width": {
|
||||
"label": "Detectar largura",
|
||||
"description": "Largura (em pixels) dos quadros usados para o fluxo de detecção; deixe em branco para usar a resolução nativa do fluxo."
|
||||
},
|
||||
"fps": {
|
||||
"label": "Detectar FPS",
|
||||
"description": "Taxa de quadros por segundo desejada para a execução da detecção; valores mais baixos reduzem o uso da CPU (o valor recomendado é 5; defina um valor mais alto — no máximo 10 — apenas se estiver rastreando objetos que se movem extremamente rápido)."
|
||||
},
|
||||
"min_initialized": {
|
||||
"label": "Quadros de inicialização mínimos",
|
||||
"description": "Número de detecções consecutivas necessárias antes de criar um objeto rastreado. Aumente esse valor para reduzir inicializações falsas. O valor padrão é o fps dividido por 2."
|
||||
},
|
||||
"max_disappeared": {
|
||||
"label": "Máximo de quadros perdidos",
|
||||
"description": "Número de quadros sem detecção antes que um objeto rastreado seja considerado desaparecido."
|
||||
},
|
||||
"stationary": {
|
||||
"label": "Configuração de objetos estáticos",
|
||||
"description": "Configurações para detectar e gerenciar objetos que permanecem parados por um período de tempo.",
|
||||
"interval": {
|
||||
"label": "Intervalo estacionário",
|
||||
"description": "Com que frequência (em quadros) realizar uma verificação de detecção para confirmar um objeto estacionário."
|
||||
},
|
||||
"threshold": {
|
||||
"label": "Limiar estacionário",
|
||||
"description": "Número de quadros sem alteração de posição necessários para marcar um objeto como estacionário."
|
||||
},
|
||||
"max_frames": {
|
||||
"label": "Quadros máximos",
|
||||
"description": "Limita por quanto tempo objetos estacionários são rastreados antes de serem descartados.",
|
||||
"default": {
|
||||
"label": "Máximo de quadros padrão",
|
||||
"description": "Número máximo padrão de quadros para rastrear um objeto estacionário antes de parar."
|
||||
},
|
||||
"objects": {
|
||||
"label": "Quadros máximos do objeto",
|
||||
"description": "Substituições por objeto para o número máximo de quadros de rastreamento de objetos estacionários."
|
||||
}
|
||||
},
|
||||
"classifier": {
|
||||
"label": "Ativar classificador visual",
|
||||
"description": "Use um classificador visual para detectar objetos verdadeiramente estacionários, mesmo quando as caixas delimitadoras oscilam."
|
||||
}
|
||||
},
|
||||
"annotation_offset": {
|
||||
"label": "Deslocamento da anotação",
|
||||
"description": "Milissegundos para deslocar as anotações de detecção, a fim de alinhar melhor as caixas delimitadoras da linha do tempo com as gravações; pode ser um valor positivo ou negativo."
|
||||
},
|
||||
"label": "Detecção de Objetos",
|
||||
"description": "Configurações para a função de detecção (*detect*) usada para executar a detecção de objetos e inicializar rastreadores."
|
||||
},
|
||||
"face_recognition": {
|
||||
"label": "Reconhecimento facial",
|
||||
"description": "Configurações de detecção e reconhecimento facial para esta câmera.",
|
||||
"enabled": {
|
||||
"label": "Ativar reconhecimento facial",
|
||||
"description": "Ative ou desative o reconhecimento facial."
|
||||
},
|
||||
"min_area": {
|
||||
"label": "Área mínima do rosto",
|
||||
"description": "Área mínima (em pixels) da caixa delimitadora de um rosto detectado necessária para tentar o reconhecimento."
|
||||
}
|
||||
},
|
||||
"ffmpeg": {
|
||||
"label": "Fluxos (FFmpeg)",
|
||||
"description": "Entradas de fluxo de câmera e opções do FFmpeg, incluindo caminho do binário, argumentos, aceleração de hardware (hwaccel) e argumentos de saída por função.",
|
||||
"path": {
|
||||
"label": "Caminho do FFmpeg",
|
||||
"description": "Caminho para o binário do FFmpeg a ser utilizado ou um alias de versão (\"7.0\" ou \"8.0\")."
|
||||
},
|
||||
"global_args": {
|
||||
"label": "Argumentos globais do FFmpeg",
|
||||
"description": "Argumentos globais passados para processos do FFmpeg."
|
||||
},
|
||||
"hwaccel_args": {
|
||||
"label": "Argumentos de aceleração de hardware",
|
||||
"description": "Argumentos de aceleração de hardware para o FFmpeg. Recomenda-se o uso de predefinições específicas do provedor."
|
||||
},
|
||||
"input_args": {
|
||||
"label": "Argumentos de entrada"
|
||||
}
|
||||
},
|
||||
"birdseye": {
|
||||
"mode": {
|
||||
"label": "Modo de rastreamento",
|
||||
"description": "Modo de inclusão de câmeras no Birdseye: 'objetos', 'movimento' ou 'contínuo'."
|
||||
},
|
||||
"order": {
|
||||
"label": "Posição",
|
||||
"description": "Posição numérica que controla a ordem da câmera no layout de vista aérea (Birdseye)."
|
||||
},
|
||||
"label": "Birdseye",
|
||||
"description": "Configurações para a visualização composta \"Birdseye\", que combina múltiplas transmissões de câmera em um único layout.",
|
||||
"enabled": {
|
||||
"label": "Ativar Birdseye"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,11 +34,15 @@
|
||||
},
|
||||
"listen": {
|
||||
"label": "Tipos de escuta",
|
||||
"description": "Lista de tipos de eventos de áudio a serem detectados (por exemplo: latido, alarme de incêndio, grito, fala, berro)."
|
||||
"description": "Lista de tipos de eventos de áudio a serem detectados (por exemplo: latido, alarme de incêndio, fala, grito)."
|
||||
},
|
||||
"filters": {
|
||||
"label": "Filtros de áudio",
|
||||
"description": "Configurações de filtro por tipo de áudio, como limites de confiança, usadas para reduzir falsos positivos."
|
||||
"description": "Configurações de filtro por tipo de áudio, como limites de confiança, usadas para reduzir falsos positivos.",
|
||||
"threshold": {
|
||||
"label": "Confiança mínima do áudio",
|
||||
"description": "Limite mínimo de confiança para que o evento de áudio seja contabilizado."
|
||||
}
|
||||
},
|
||||
"enabled_in_config": {
|
||||
"label": "Estado de áudio original",
|
||||
@@ -47,7 +51,8 @@
|
||||
"num_threads": {
|
||||
"label": "Threads de detecção",
|
||||
"description": "Número de threads a serem usadas para o processamento de detecção de áudio."
|
||||
}
|
||||
},
|
||||
"label": "Detecção de áudio"
|
||||
},
|
||||
"auth": {
|
||||
"label": "Autenticação",
|
||||
@@ -82,8 +87,110 @@
|
||||
"audio_transcription": {
|
||||
"label": "Transcrição de áudio",
|
||||
"live_enabled": {
|
||||
"label": "Transcrição em tempo real"
|
||||
"label": "Transcrição em tempo real",
|
||||
"description": "Ative a transcrição ao vivo por streaming para o áudio à medida que ele é recebido."
|
||||
},
|
||||
"description": "Configurações de transcrição de áudio e voz ao vivo para eventos e legendas em tempo real."
|
||||
},
|
||||
"detect": {
|
||||
"enabled": {
|
||||
"label": "Ativar detecção de objetos"
|
||||
},
|
||||
"height": {
|
||||
"label": "Detectar altura",
|
||||
"description": "Altura (em pixels) dos quadros usados para o fluxo de detecção; deixe em branco para usar a resolução nativa do fluxo."
|
||||
},
|
||||
"width": {
|
||||
"label": "Detectar largura",
|
||||
"description": "Largura (em pixels) dos quadros usados para o fluxo de detecção; deixe em branco para usar a resolução nativa do fluxo."
|
||||
},
|
||||
"fps": {
|
||||
"label": "Detectar FPS",
|
||||
"description": "Taxa de quadros por segundo desejada para a execução da detecção; valores mais baixos reduzem o uso da CPU (o valor recomendado é 5; defina um valor mais alto — no máximo 10 — apenas se estiver rastreando objetos que se movem extremamente rápido)."
|
||||
},
|
||||
"min_initialized": {
|
||||
"label": "Quadros de inicialização mínimos",
|
||||
"description": "Número de detecções consecutivas necessárias antes de criar um objeto rastreado. Aumente esse valor para reduzir inicializações falsas. O valor padrão é o fps dividido por 2."
|
||||
},
|
||||
"max_disappeared": {
|
||||
"label": "Máximo de quadros perdidos",
|
||||
"description": "Número de quadros sem detecção antes que um objeto rastreado seja considerado desaparecido."
|
||||
},
|
||||
"stationary": {
|
||||
"label": "Configuração de objetos estáticos",
|
||||
"description": "Configurações para detectar e gerenciar objetos que permanecem parados por um período de tempo.",
|
||||
"interval": {
|
||||
"label": "Intervalo estacionário",
|
||||
"description": "Com que frequência (em quadros) realizar uma verificação de detecção para confirmar um objeto estacionário."
|
||||
},
|
||||
"threshold": {
|
||||
"label": "Limiar estacionário",
|
||||
"description": "Número de quadros sem alteração de posição necessários para marcar um objeto como estacionário."
|
||||
},
|
||||
"max_frames": {
|
||||
"label": "Quadros máximos",
|
||||
"description": "Limita por quanto tempo objetos estacionários são rastreados antes de serem descartados.",
|
||||
"default": {
|
||||
"label": "Máximo de quadros padrão",
|
||||
"description": "Número máximo padrão de quadros para rastrear um objeto estacionário antes de parar."
|
||||
},
|
||||
"objects": {
|
||||
"label": "Quadros máximos do objeto",
|
||||
"description": "Substituições por objeto para o número máximo de quadros de rastreamento de objetos estacionários."
|
||||
}
|
||||
},
|
||||
"classifier": {
|
||||
"label": "Ativar classificador visual",
|
||||
"description": "Use um classificador visual para detectar objetos verdadeiramente estacionários, mesmo quando as caixas delimitadoras oscilam."
|
||||
}
|
||||
},
|
||||
"annotation_offset": {
|
||||
"label": "Deslocamento da anotação",
|
||||
"description": "Milissegundos para deslocar as anotações de detecção, a fim de alinhar melhor as caixas delimitadoras da linha do tempo com as gravações; pode ser um valor positivo ou negativo."
|
||||
},
|
||||
"label": "Detecção de Objetos",
|
||||
"description": "Configurações para a função de detecção (*detect*) usada para executar a detecção de objetos e inicializar rastreadores."
|
||||
},
|
||||
"face_recognition": {
|
||||
"label": "Reconhecimento facial",
|
||||
"enabled": {
|
||||
"label": "Ativar reconhecimento facial"
|
||||
},
|
||||
"min_area": {
|
||||
"label": "Área mínima do rosto",
|
||||
"description": "Área mínima (em pixels) da caixa delimitadora de um rosto detectado necessária para tentar o reconhecimento."
|
||||
}
|
||||
},
|
||||
"ffmpeg": {
|
||||
"path": {
|
||||
"label": "Caminho do FFmpeg",
|
||||
"description": "Caminho para o binário do FFmpeg a ser utilizado ou um alias de versão (\"7.0\" ou \"8.0\")."
|
||||
},
|
||||
"global_args": {
|
||||
"label": "Argumentos globais do FFmpeg",
|
||||
"description": "Argumentos globais passados para processos do FFmpeg."
|
||||
},
|
||||
"hwaccel_args": {
|
||||
"label": "Argumentos de aceleração de hardware",
|
||||
"description": "Argumentos de aceleração de hardware para o FFmpeg. Recomenda-se o uso de predefinições específicas do provedor."
|
||||
},
|
||||
"input_args": {
|
||||
"label": "Argumentos de entrada"
|
||||
}
|
||||
},
|
||||
"birdseye": {
|
||||
"mode": {
|
||||
"label": "Modo de rastreamento",
|
||||
"description": "Modo de inclusão de câmeras no Birdseye: 'objetos', 'movimento' ou 'contínuo'."
|
||||
},
|
||||
"order": {
|
||||
"label": "Posição",
|
||||
"description": "Posição numérica que controla a ordem da câmera no layout de vista aérea (Birdseye)."
|
||||
},
|
||||
"label": "Birdseye",
|
||||
"description": "Configurações para a visualização composta \"Birdseye\", que combina múltiplas transmissões de câmera em um único layout.",
|
||||
"enabled": {
|
||||
"label": "Ativar Birdseye"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -64,5 +64,10 @@
|
||||
"retention": "Retenção",
|
||||
"events": "Eventos"
|
||||
}
|
||||
},
|
||||
"ffmpeg": {
|
||||
"cameras": {
|
||||
"cameraFfmpeg": "Argumentos do FFmpeg específicos para a câmera"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,5 +28,8 @@
|
||||
"detectRequired": "Pelo menos um fluxo de entrada deve ter a função 'detectar' atribuída.",
|
||||
"hwaccelDetectOnly": "Somente o fluxo de entrada com a função de detecção pode definir argumentos de aceleração de hardware."
|
||||
}
|
||||
},
|
||||
"detect": {
|
||||
"dimensionMustBeEven": "Deve ser um número par."
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1 +1,63 @@
|
||||
{}
|
||||
{
|
||||
"documentTitle": "Frigate-Chat",
|
||||
"title": "Chat-Frigate",
|
||||
"subtitle": "Seu assistente de IA para gerenciamento de câmeras e insights",
|
||||
"placeholder": "Pergunte o que quiser...",
|
||||
"error": "Algo deu errado. Por favor, tente novamente.",
|
||||
"processing": "Processando...",
|
||||
"toolsUsed": "Usado: {{tools}}",
|
||||
"showTools": "Mostrar ferramentas ({{count}})",
|
||||
"hideTools": "Ocultar ferramentas",
|
||||
"call": "Chamar",
|
||||
"result": "Resultado",
|
||||
"arguments": "Argumentos:",
|
||||
"response": "Resposta:",
|
||||
"attachment_chip_label": "{{label}} em {{camera}}",
|
||||
"attachment_chip_remove": "Remover anexo",
|
||||
"open_in_explore": "Abrir no Explorar",
|
||||
"attach_event_aria": "Anexar evento {{eventId}}",
|
||||
"attachment_picker_paste_label": "Ou cole o ID do evento",
|
||||
"attachment_picker_attach": "Anexar",
|
||||
"attachment_picker_placeholder": "Anexar um evento",
|
||||
"quick_reply_find_similar": "Encontre avistamentos semelhantes",
|
||||
"quick_reply_tell_me_more": "Conte-me mais sobre isso",
|
||||
"quick_reply_when_else": "Em que outra ocasião isso foi visto?",
|
||||
"quick_reply_find_similar_text": "Encontre avistamentos semelhantes a este.",
|
||||
"quick_reply_tell_me_more_text": "Conte-me mais sobre este.",
|
||||
"quick_reply_when_else_text": "Em que outra ocasião isso foi visto?",
|
||||
"anchor": "Referência",
|
||||
"similarity_score": "Semelhança",
|
||||
"no_similar_objects_found": "Nenhum objeto semelhante encontrado.",
|
||||
"semantic_search_required": "A busca semântica deve estar habilitada para encontrar objetos semelhantes.",
|
||||
"send": "Enviar",
|
||||
"suggested_requests": "Experimente perguntar:",
|
||||
"starting_requests": {
|
||||
"show_recent_events": "Mostrar eventos recentes",
|
||||
"show_camera_status": "Mostrar status da câmera",
|
||||
"recap": "O que aconteceu enquanto eu estava fora?",
|
||||
"watch_camera": "Monitore uma câmera em busca de atividade"
|
||||
},
|
||||
"starting_requests_prompts": {
|
||||
"show_recent_events": "Mostre-me os eventos recentes da última hora",
|
||||
"show_camera_status": "Qual é o status atual das minhas câmeras?",
|
||||
"recap": "O que aconteceu enquanto eu estava fora?",
|
||||
"watch_camera": "Fique de olho na porta da frente e me avise se alguém aparecer"
|
||||
},
|
||||
"new_chat": "Novo chat",
|
||||
"settings": {
|
||||
"title": "Configurações do chat",
|
||||
"show_stats": {
|
||||
"title": "Mostrar estatísticas",
|
||||
"desc": "Exibir a taxa de geração e o tamanho do contexto para respostas de chat.",
|
||||
"while_generating": "Durante a geração",
|
||||
"always": "Sempre"
|
||||
},
|
||||
"auto_scroll": {
|
||||
"title": "Rolagem automática",
|
||||
"desc": "Acompanhe as novas mensagens à medida que chegam."
|
||||
}
|
||||
},
|
||||
"stats": {
|
||||
"context": "{{tokens}} tokens"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
"toast": {
|
||||
"success": {
|
||||
"deletedCategory_one": "Classe Apagada",
|
||||
"deletedCategory_many": "",
|
||||
"deletedCategory_many": "Classes apagadas",
|
||||
"deletedCategory_other": "",
|
||||
"deletedImage_one": "Imagen Apagada",
|
||||
"deletedImage_many": "Imagens Apagadas",
|
||||
@@ -94,7 +94,14 @@
|
||||
},
|
||||
"noModels": {
|
||||
"object": {
|
||||
"title": "Nenhum Modelo de Classificação de Objetos"
|
||||
"title": "Nenhum Modelo de Classificação de Objetos",
|
||||
"description": "Crie um modelo personalizado para classificar objetos detectados.",
|
||||
"buttonText": "Criar modelo de objeto"
|
||||
},
|
||||
"state": {
|
||||
"title": "Modelos de Classificação Sem Estado",
|
||||
"description": "Crie um modelo personalizado para monitorar e classificar mudanças de estado em áreas específicas da câmera.",
|
||||
"buttonText": "Criar modelo de estado"
|
||||
}
|
||||
},
|
||||
"deleteDatasetImages": {
|
||||
@@ -104,5 +111,13 @@
|
||||
"title": "Editar Modelo de Classificação",
|
||||
"descriptionState": "Edite as classes deste modelo de classificação de estado. As alterações exigirão treinar novamente o modelo.",
|
||||
"descriptionObject": "Edite o tipo de objeto e tipo de classificação para este modelo de classificação de objeto."
|
||||
},
|
||||
"reclassifyImageAs": "Reclassificar imagem como:",
|
||||
"reclassifyImage": "Reclassificar imagem",
|
||||
"wizard": {
|
||||
"title": "Criar nova classificação",
|
||||
"steps": {
|
||||
"nameAndDefine": "Nomear e definir"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -72,7 +72,11 @@
|
||||
"title": "Reconhecimentos Recentes",
|
||||
"aria": "Selecionar reconhecimentos recentes",
|
||||
"empty": "Não há tentativas recentes de reconhecimento facial",
|
||||
"titleShort": "Recente"
|
||||
"titleShort": "Recente",
|
||||
"emptyNoLibrary": {
|
||||
"title": "Carregue uma foto de rosto",
|
||||
"description": "Você deve adicionar pelo menos um rosto à biblioteca para que o reconhecimento facial funcione."
|
||||
}
|
||||
},
|
||||
"selectFace": "Selecionar Rosto",
|
||||
"trainFaceAs": "Treinar Rosto como:",
|
||||
@@ -89,7 +93,8 @@
|
||||
"renamedFace": "O rosto foi renomeado com sucesso para {{name}}",
|
||||
"deletedName_one": "{{count}} rosto foi deletado com sucesso.",
|
||||
"deletedName_many": "{{count}} rostos foram deletados com sucesso.",
|
||||
"deletedName_other": "{{count}} rostos foram deletados com sucesso."
|
||||
"deletedName_other": "{{count}} rostos foram deletados com sucesso.",
|
||||
"reclassifiedFace": "Rosto reclassificado com sucesso."
|
||||
},
|
||||
"error": {
|
||||
"uploadingImageFailed": "Falha ao enviar a imagem: {{errorMessage}}",
|
||||
@@ -98,7 +103,10 @@
|
||||
"deleteNameFailed": "Falha ao deletar nome: {{errorMessage}}",
|
||||
"renameFaceFailed": "Falha ao renomear rosto: {{errorMessage}}",
|
||||
"trainFailed": "Falha ao treinar: {{errorMessage}}",
|
||||
"updateFaceScoreFailed": "Falha ao atualizar pontuação de rosto: {{errorMessage}}"
|
||||
"updateFaceScoreFailed": "Falha ao atualizar pontuação de rosto: {{errorMessage}}",
|
||||
"reclassifyFailed": "Falha ao reclassificar o rosto: {{errorMessage}}"
|
||||
}
|
||||
}
|
||||
},
|
||||
"reclassifyFaceAs": "Reclassificar rosto como:",
|
||||
"reclassifyFace": "Reclassificar rosto"
|
||||
}
|
||||
|
||||
@@ -58,7 +58,9 @@
|
||||
},
|
||||
"camera": {
|
||||
"enable": "Ativar Câmera",
|
||||
"disable": "Desabilitar Câmera"
|
||||
"disable": "Desabilitar Câmera",
|
||||
"turnOn": "Ligar a câmera",
|
||||
"turnOff": "Desligar a câmera"
|
||||
},
|
||||
"muteCameras": {
|
||||
"enable": "Silenciar Todas as Câmeras",
|
||||
@@ -70,7 +72,8 @@
|
||||
},
|
||||
"recording": {
|
||||
"enable": "Ativar Gravação",
|
||||
"disable": "Desativar Gravação"
|
||||
"disable": "Desativar Gravação",
|
||||
"disabledInConfig": "A gravação deve ser habilitada primeiro nas configurações desta câmera."
|
||||
},
|
||||
"snapshots": {
|
||||
"enable": "Permitir Capturas de Imagem",
|
||||
@@ -150,7 +153,8 @@
|
||||
"snapshots": "Capturas de Imagem",
|
||||
"audioDetection": "Detecção de Áudio",
|
||||
"autotracking": "Auto Rastreamento",
|
||||
"transcription": "Transcrição de Áudio"
|
||||
"transcription": "Transcrição de Áudio",
|
||||
"camera": "Câmera"
|
||||
},
|
||||
"history": {
|
||||
"label": "Exibir gravação histórica"
|
||||
|
||||
@@ -242,7 +242,8 @@
|
||||
"id": "Indonésio Bahasa",
|
||||
"ur": "Urdu",
|
||||
"hr": "Hrvatski (Croata)",
|
||||
"bs": "Bosanski (Bósnio)"
|
||||
"bs": "Bosanski (Bósnio)",
|
||||
"zhHant": "繁體中文 (Chinês Tradicional)"
|
||||
},
|
||||
"appearance": "Aparência",
|
||||
"darkMode": {
|
||||
|
||||
@@ -68,7 +68,10 @@
|
||||
"stream": "Transmissão"
|
||||
},
|
||||
"birdseye": "Vista Aérea"
|
||||
}
|
||||
},
|
||||
"showAll": "Mostrar todos os Grupos de Câmeras",
|
||||
"showLess": "Mostrar Menos",
|
||||
"editGroups": "Editar Grupos de Câmeras"
|
||||
},
|
||||
"debug": {
|
||||
"options": {
|
||||
@@ -82,6 +85,7 @@
|
||||
"zones": "Zonas",
|
||||
"mask": "Máscara",
|
||||
"motion": "Movimento",
|
||||
"regions": "Regiões"
|
||||
"regions": "Regiões",
|
||||
"paths": "Caminhos"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -36,6 +36,9 @@
|
||||
"ask_a": "Este objeto é um <code>{{label}}</code>?",
|
||||
"ask_an": "Este objeto é um <code>{{label}}</code>?",
|
||||
"ask_full": "Este objeto é um <code>{{untranslatedLabel}}</code> ({{translatedLabel}})?"
|
||||
},
|
||||
"toast": {
|
||||
"error": "Não foi possível submeter para Frigate+. Por favor verifique a sua connecção e tente novamente."
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -76,6 +79,13 @@
|
||||
"select": "Selecionar",
|
||||
"name": {
|
||||
"placeholder": "Nome da Exportação"
|
||||
},
|
||||
"case": {
|
||||
"newCaseOption": "Criar um novo caso",
|
||||
"newCaseNamePlaceholder": "Nome do novo caso"
|
||||
},
|
||||
"multiCamera": {
|
||||
"selectFromTimeline": "Selecionar da Timeline"
|
||||
}
|
||||
},
|
||||
"streaming": {
|
||||
|
||||
@@ -20,6 +20,9 @@
|
||||
},
|
||||
"state": {
|
||||
"submitted": "已提交"
|
||||
},
|
||||
"toast": {
|
||||
"error": "無法提交至 Frigate+,請檢查您的網路連線並再試一次。"
|
||||
}
|
||||
},
|
||||
"submitToPlus": {
|
||||
|
||||
@@ -697,7 +697,7 @@
|
||||
"description": "在接收到音訊時開啟即時監控持續轉寫。"
|
||||
},
|
||||
"enabled": {
|
||||
"label": "開啟轉錄",
|
||||
"label": "啟用語音轉文字",
|
||||
"description": "開啟或關閉手動觸發的音訊事件轉寫。"
|
||||
},
|
||||
"enabled_in_config": {
|
||||
|
||||
@@ -5,7 +5,8 @@ import { LiveStreamMetadata } from "@/types/live";
|
||||
|
||||
const FETCH_TIMEOUT_MS = 10000;
|
||||
const DEFER_DELAY_MS = 2000;
|
||||
const EMPTY_METADATA: { [key: string]: LiveStreamMetadata } = {};
|
||||
const emptyObject: Readonly<{ [key: string]: LiveStreamMetadata }> =
|
||||
Object.freeze({});
|
||||
|
||||
/**
|
||||
* Hook that fetches go2rtc stream metadata with deferred loading.
|
||||
@@ -78,7 +79,7 @@ export default function useDeferredStreamMetadata(streamNames: string[]) {
|
||||
return metadata;
|
||||
}, []);
|
||||
|
||||
const { data: metadata = EMPTY_METADATA } = useSWR<{
|
||||
const { data: metadata = emptyObject } = useSWR<{
|
||||
[key: string]: LiveStreamMetadata;
|
||||
}>(swrKey, fetcher, {
|
||||
revalidateOnFocus: false,
|
||||
|
||||
Reference in New Issue
Block a user