Compare commits

..
Author SHA1 Message Date
dependabot[bot]andGitHub 4b9fa289f2 Update tensorflow requirement from ==2.19.* to ==2.21.* in /docker/main
Updates the requirements on [tensorflow](https://github.com/tensorflow/tensorflow) to permit the latest version.
- [Release notes](https://github.com/tensorflow/tensorflow/releases)
- [Changelog](https://github.com/tensorflow/tensorflow/blob/master/RELEASE.md)
- [Commits](https://github.com/tensorflow/tensorflow/compare/v2.19.0-rc0...v2.21.0)

---
updated-dependencies:
- dependency-name: tensorflow
  dependency-version: 2.21.0
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-09-25 11:32:43 +00:00
190 changed files with 2204 additions and 13686 deletions
+1 -1
View File
@@ -13,7 +13,7 @@ pathvalidate == 3.3.*
markupsafe == 3.0.*
python-multipart == 0.0.31
# Classification Model Training
tensorflow == 2.19.* ; platform_machine == 'aarch64'
tensorflow == 2.21.* ; platform_machine == 'aarch64'
tensorflow-cpu == 2.19.* ; platform_machine == 'x86_64'
# General
mypy == 1.6.1
@@ -85,14 +85,6 @@ An optional config, `save_attempts`, can be set as a key under the model name. T
</TabItem>
</ConfigTabs>
## Review items
When a model's state changes while its camera has an active review item, the change is recorded on that review item. This includes changes in the few seconds before the item starts, such as a garage door opening just before the car is detected. State changes never create or extend review items on their own, and the first state reported after Frigate starts is not recorded as a change.
Recorded changes appear in the review item's data as `classification_state_changes` (see the [`frigate/reviews`](/integrations/mqtt#frigatereviews) MQTT topic) and are passed to [GenAI review summaries](/configuration/genai/genai_review) as facts, so a description can note that a gate was opened during the activity.
Change times are most accurate with `motion: true`. A model that only runs on an `interval` notices a change at its next run, so the change may be recorded late or attached to a later review item.
## Training the model
Creating and training the model is done within the Frigate UI using the `Classification` page. The process consists of three steps:
@@ -201,8 +201,6 @@ Review items are sent to the model as a sequence of still frames. Some models fo
The notes come from tracking data rather than from the images, so they describe activity the model may not have picked up on its own. In testing with a person carrying three waste bins to the curb one at a time, `gemma4` described a single trip on every attempt with `frames`, and consistently described multiple trips with `annotated_frames`. Models that already handle these sequences well, such as the `qwen3-vl` family, gain little and should stay on `frames`.
Changes reported by [state classification](/configuration/custom_classification/state_classification#review-items) models during the review item are listed in the prompt in both modes. `annotated_frames` also notes each change before the frame where it happened.
Annotated mode also caps the number of frames, since the notes already establish the order of events and extra near-duplicate frames tend to crowd out the middle of a clip. Longer review items are sampled more sparsely as a result, and typically use fewer tokens than `frames` mode for the same item.
:::note
+2 -13
View File
@@ -212,7 +212,6 @@ An `update` with the same ID will be published when:
- The severity changes from `detection` to `alert`
- Additional objects are detected
- An object is recognized via face, lpr, etc.
- A [state classification](/configuration/custom_classification/state_classification#review-items) model changes state
When the review activity has ended a final `end` message is published.
@@ -236,8 +235,7 @@ When the review activity has ended a final `end` message is published.
"objects": ["person", "car"],
"sub_labels": [],
"zones": [],
"audio": [],
"classification_state_changes": []
"audio": []
}
},
"after": {
@@ -256,16 +254,7 @@ When the review activity has ended a final `end` message is published.
"objects": ["person", "car"],
"sub_labels": ["Bob"],
"zones": ["front_yard"],
"audio": [],
"classification_state_changes": [
// verified changes of state classification models on this camera
{
"model": "front_gate",
"from": "closed",
"to": "open",
"timestamp": 1718987131.52
}
]
"audio": []
}
}
}
-5
View File
@@ -393,11 +393,6 @@ def config(request: Request):
model_dict["non_logo_attributes"] = model.non_logo_attributes
model_dict["labelmap"] = model.merged_labelmap
# report the configured reference rather than the resolved cache path,
# so saving the config back doesn't lose the Frigate+ model
if model.plus_id:
model_dict["path"] = f"plus://{model.plus_id}"
if not config["plus"]["enabled"]:
continue
-1
View File
@@ -51,7 +51,6 @@ def swap_runtime_config(app: FastAPI, config: FrigateConfig) -> None:
if app.stats_emitter is not None:
app.stats_emitter.config = config
app.stats_emitter.hardware_stats.set_config(config)
if app.dispatcher is not None:
app.dispatcher.config = config
+7 -1
View File
@@ -63,6 +63,7 @@ from frigate.util.recording_coverage import (
null_audio_glitches,
plan_clip,
resolve_coverage,
stream_has_audio,
)
logger = logging.getLogger(__name__)
@@ -680,10 +681,15 @@ async def _vod_response(
end_ts,
force_discontinuity,
)
intervals = resolve_coverage(camera_name, start_ts, end_ts)
# rows contradicting their stream's audio composition are
# truncated-shutdown glitches
main_audio = stream_has_audio(intervals, main=True)
sub_audio = stream_has_audio(intervals, main=False)
spans = build_spans(
null_audio_glitches(resolve_coverage(camera_name, start_ts, end_ts)),
null_audio_glitches(intervals, main_audio, sub_audio),
stream_preference,
)
-1
View File
@@ -12,7 +12,6 @@ class DetectionTypeEnum(str, Enum):
video = "video"
audio = "audio"
lpr = "lpr"
classification_state = "classification_state"
class DetectionPublisher(Publisher):
@@ -1,10 +1,10 @@
"""Frame annotations derived from object tracking data.
Builds short notes describing what changed during a review item, keyed to the
frames sampled from it. Everything here comes from data already recorded
(each event's `path_data` trajectory, the timeline's stationary/active
changes, and the review item's state classification changes), so the notes
can be stated to the model as fact rather than as something it must perceive.
frames sampled from it. Everything here comes from tracked object data already
in the database (each event's `path_data` trajectory and the timeline's
stationary/active changes), so the notes can be stated to the model as fact
rather than as something it must perceive.
"""
import logging
@@ -95,15 +95,6 @@ def event_name(event: dict[str, Any]) -> str:
return f"{article} {label}"
def describe_classification_change(change: dict[str, Any]) -> str:
"""Phrase a state classification change, e.g. 'front gate changed from
closed to open'."""
model = str(change["model"]).replace("_", " ")
before = str(change["from"]).replace("_", " ")
after = str(change["to"]).replace("_", " ")
return f"{model} changed from {before} to {after}"
def path_legs(points: list[Point]) -> list[Leg]:
"""Split a trajectory into runs of travel in a consistent direction.
@@ -368,38 +359,25 @@ def get_state_changes(detection_ids: list[str]) -> list[dict[str, Any]]:
def build_frame_captions(
detection_ids: list[str],
frame_times: list[float],
classification_changes: Sequence[dict[str, Any]] = (),
) -> list[str]:
"""A caption for each sampled frame, in frame order.
Every frame gets its index and elapsed time so the model can tell them
apart; frames where something changed also carry the tracker and state
classification notes for that moment. Returns an empty list when there is
nothing to say, which callers treat as a reason to fall back to sending
plain frames.
apart; frames where something changed also carry the tracker notes for
that moment. Returns an empty list when there is nothing to say, which
callers treat as a reason to fall back to sending plain frames.
"""
if not frame_times:
return []
span_end = frame_times[-1]
timeline: list[tuple[float, str]] = []
events = get_tracked_events(detection_ids)
# audio and manual review items can have state changes but no tracked objects
if events:
timeline.extend(
(timestamp, f"[tracker] {note}")
for timestamp, note in build_timeline(
events, span_end, get_state_changes(detection_ids)
)
)
if not events:
logger.debug("No tracked events found for review item, skipping annotations")
return []
timeline.extend(
(change["timestamp"], f"[state] {describe_classification_change(change)}")
for change in classification_changes
if change["timestamp"] <= span_end
)
buckets = annotations_by_frame(sorted(timeline, key=lambda m: m[0]), frame_times)
timeline = build_timeline(events, frame_times[-1], get_state_changes(detection_ids))
buckets = annotations_by_frame(timeline, frame_times)
if not buckets:
return []
@@ -410,7 +388,7 @@ def build_frame_captions(
for index, timestamp in enumerate(frame_times):
lines = [f"Frame {index + 1} of {total} (+{timestamp - origin:.1f}s):"]
lines.extend(buckets.get(index, []))
lines.extend(f"[tracker] {note}" for note in buckets.get(index, []))
captions.append("\n".join(lines))
return captions
@@ -40,7 +40,7 @@ from frigate.util.image import get_image_from_recording
from ..post.api import PostProcessorApi
from ..types import DataProcessorMetrics
from .review_annotations import build_frame_captions, describe_classification_change
from .review_annotations import build_frame_captions
logger = logging.getLogger(__name__)
@@ -254,10 +254,6 @@ class ReviewDescriptionProcessor(PostProcessorApi):
"start_time": r["start_time"],
"end_time": r["end_time"],
"metadata": r["data"]["metadata"],
"state_changes": [
describe_classification_change(change)
for change in sorted_classification_state_changes(r["data"])
],
}
for r in (
ReviewSegment.select(
@@ -302,9 +298,6 @@ class ReviewDescriptionProcessor(PostProcessorApi):
primary_item["start_time"] = primary_seg["start_time"]
primary_item["end_time"] = primary_seg["end_time"]
if primary_seg["state_changes"]:
primary_item["state_changes"] = primary_seg["state_changes"]
# Find overlapping contextual items from other cameras
primary_start = primary_seg["start_time"]
primary_end = primary_seg["end_time"]
@@ -325,25 +318,14 @@ class ReviewDescriptionProcessor(PostProcessorApi):
seg_end = seg["end_time"]
if seg_start < primary_end and primary_start < seg_end:
# Avoid duplicates if same camera has multiple overlapping
# segments. One with state changes is kept as its own item
# so each change stays within its item's time range.
if (
seg_camera in seen_contextual_cameras
and not seg["state_changes"]
):
continue
contextual_item = copy.deepcopy(seg["metadata"])
contextual_item["camera"] = seg_camera
contextual_item["start_time"] = seg_start
contextual_item["end_time"] = seg_end
if seg["state_changes"]:
contextual_item["state_changes"] = seg["state_changes"]
contextual_items.append(contextual_item)
seen_contextual_cameras.add(seg_camera)
# Avoid duplicates if same camera has multiple overlapping segments
if seg_camera not in seen_contextual_cameras:
contextual_item = copy.deepcopy(seg["metadata"])
contextual_item["camera"] = seg_camera
contextual_item["start_time"] = seg_start
contextual_item["end_time"] = seg_end
contextual_items.append(contextual_item)
seen_contextual_cameras.add(seg_camera)
# Add context array to primary item
primary_item["context"] = contextual_items
@@ -457,7 +439,6 @@ class ReviewDescriptionProcessor(PostProcessorApi):
captions = build_frame_captions(
final_data["data"].get("detections") or [],
[timestamp for _, timestamp in frames],
sorted_classification_state_changes(final_data["data"]),
)
if not captions:
@@ -707,39 +688,6 @@ def get_recording_buffer_extension(duration: float) -> float:
return buffer_extension
def sorted_classification_state_changes(
review_data: dict[str, Any],
) -> list[dict[str, Any]]:
"""A review item's state classification changes in time order."""
return sorted(
review_data.get("classification_state_changes") or [],
key=lambda change: change["timestamp"],
)
def format_classification_state_changes(
changes: list[dict[str, Any]], start_time: float, end_time: float
) -> list[str]:
"""Phrase state classification changes with their timing in the activity.
Changes are attached while the review item is active, which runs past its
end_time by the review cutoff, and a few seconds before its start.
"""
lines = []
for change in changes:
if change["timestamp"] < start_time:
when = "just before the activity started"
elif change["timestamp"] > end_time:
when = "after the activity ended"
else:
when = f"{round(change['timestamp'] - start_time)}s into the activity"
lines.append(f"{describe_classification_change(change)}, {when}")
return lines
def run_analysis(
requestor: InterProcessRequestor,
genai_client: GenAIClient,
@@ -795,13 +743,6 @@ def run_analysis(
unified_objects.append(object_type)
analytics_data["unified_objects"] = unified_objects
analytics_data["classification_state_changes"] = (
format_classification_state_changes(
sorted_classification_state_changes(final_data["data"]),
final_data["start_time"],
final_data["end_time"],
)
)
metadata = genai_client.generate_review_description(
analytics_data,
@@ -91,23 +91,8 @@ class CustomStateClassificationProcessor(DeferredRealtimeProcessorApi):
self.tensor_input_details = self.interpreter.get_input_details()
self.tensor_output_details = self.interpreter.get_output_details()
self.labelmap = load_labels(labelmap_path, prefill=0, indexed=False)
self._forget_unknown_states()
self.classifications_per_second.start()
def _forget_unknown_states(self) -> None:
"""Drop verified states that are not labels of the loaded model.
A retrained model can rename or remove labels. Keeping a state it can
no longer produce would report its first verified state as a change
from that obsolete label.
"""
labels = set(self.labelmap.values())
self.state_history = {
camera: history
for camera, history in self.state_history.items()
if history["current_state"] in labels
}
def __update_metrics(self, duration: float) -> None:
self.classifications_per_second.update()
if self.inference_speed:
@@ -149,20 +134,15 @@ class CustomStateClassificationProcessor(DeferredRealtimeProcessorApi):
# Don't save if state is stable (detected_state == current_state) AND score is 100%
return False
def verify_state_change(
self, camera: str, detected_state: str, timestamp: float
) -> tuple[str | None, float] | None:
def verify_state_change(self, camera: str, detected_state: str) -> str | None:
"""
Verify state change requires 3 consecutive identical states before publishing.
Returns (previous state, time the new state was first seen) once verified,
or None if verification not complete. The previous state is None for the
first state verified on a camera.
Returns state to publish or None if verification not complete.
"""
if camera not in self.state_history:
self.state_history[camera] = {
"current_state": None,
"pending_state": None,
"pending_since": 0.0,
"consecutive_count": 0,
}
@@ -177,14 +157,12 @@ class CustomStateClassificationProcessor(DeferredRealtimeProcessorApi):
verification["consecutive_count"] += 1
if verification["consecutive_count"] >= 3:
previous_state = verification["current_state"]
verification["current_state"] = detected_state
verification["pending_state"] = None
verification["consecutive_count"] = 0
return previous_state, verification["pending_since"]
return detected_state
else:
verification["pending_state"] = detected_state
verification["pending_since"] = timestamp
verification["consecutive_count"] = 1
logger.debug(
f"New state '{detected_state}' detected for {camera}, need {3 - verification['consecutive_count']} more consecutive detections"
@@ -362,19 +340,16 @@ class CustomStateClassificationProcessor(DeferredRealtimeProcessorApi):
)
return
verified = self.verify_state_change(camera, detected_state, timestamp)
verified_state = self.verify_state_change(camera, detected_state)
if verified is not None:
previous_state, changed_at = verified
if verified_state is not None:
self._emit_result(
{
"type": "classification",
"processor": "state",
"model_name": self.model_config.name,
"camera": camera,
"state": detected_state,
"previous_state": previous_state,
"timestamp": changed_at,
"state": verified_state,
}
)
-7
View File
@@ -123,7 +123,6 @@ class ModelConfig(BaseModel):
_all_attributes: list[str] = PrivateAttr()
_all_attribute_logos: list[str] = PrivateAttr()
_model_hash: str = PrivateAttr()
_plus_id: str | None = PrivateAttr(default=None)
@property
def merged_labelmap(self) -> dict[int, str]:
@@ -149,11 +148,6 @@ class ModelConfig(BaseModel):
def model_hash(self) -> str:
return self._model_hash
@property
def plus_id(self) -> str | None:
"""The Frigate+ model id, once a plus:// path has been resolved."""
return self._plus_id
def __init__(self, **config):
super().__init__(**config)
@@ -184,7 +178,6 @@ class ModelConfig(BaseModel):
os.makedirs(MODEL_CACHE_DIR, exist_ok=True)
model_id = self.path[7:]
self._plus_id = model_id
self.path = os.path.join(MODEL_CACHE_DIR, model_id)
model_info_path = f"{self.path}.json"
+1 -22
View File
@@ -11,11 +11,7 @@ from typing import Any
from peewee import DoesNotExist
from frigate.comms.config_updater import ConfigSubscriber
from frigate.comms.detections_updater import (
DetectionPublisher,
DetectionSubscriber,
DetectionTypeEnum,
)
from frigate.comms.detections_updater import DetectionSubscriber, DetectionTypeEnum
from frigate.comms.embeddings_updater import (
EmbeddingsRequestEnum,
EmbeddingsResponder,
@@ -172,7 +168,6 @@ class EmbeddingMaintainer(threading.Thread):
)
self.review_subscriber = ReviewDataSubscriber("")
self.detection_subscriber = DetectionSubscriber(DetectionTypeEnum.video.value)
self.detection_publisher = DetectionPublisher(DetectionTypeEnum.all.value)
self.embeddings_responder = EmbeddingsResponder()
self.frame_manager = SharedMemoryFrameManager()
@@ -361,7 +356,6 @@ class EmbeddingMaintainer(threading.Thread):
self.event_end_subscriber.stop()
self.recordings_subscriber.stop()
self.detection_subscriber.stop()
self.detection_publisher.stop()
self.event_metadata_publisher.stop()
self.event_metadata_subscriber.stop()
self.embeddings_responder.stop()
@@ -857,21 +851,6 @@ class EmbeddingMaintainer(threading.Thread):
f"{result['camera']}/classification/{result['model_name']}",
result["state"],
)
# the first state verified after startup is not a change
if result["previous_state"] is not None:
self.detection_publisher.publish(
(
result["camera"],
{
"model": result["model_name"],
"from": result["previous_state"],
"to": result["state"],
"timestamp": result["timestamp"],
},
),
DetectionTypeEnum.classification_state.value,
)
elif result["processor"] == "object":
object_id = result["object_id"]
camera = result["camera"]
+89 -36
View File
@@ -103,10 +103,11 @@ class LlamaCppClient(GenAIClient):
_supports_reasoning: bool
_image_token_cache: dict[tuple[int, int], int]
_text_baseline_tokens: int | None
_media_marker: str
@property
def supports_embeddings(self) -> bool:
"""llama.cpp exposes a /v1/embeddings endpoint for any loaded model."""
"""llama.cpp exposes an /embeddings endpoint for any loaded model."""
return True
def _auth_headers(self) -> dict | None:
@@ -158,6 +159,7 @@ class LlamaCppClient(GenAIClient):
self._supports_reasoning = False
self._image_token_cache = {}
self._text_baseline_tokens = None
self._media_marker = "<__media__>"
base_url = (
self.genai_config.base_url.rstrip("/")
@@ -185,6 +187,7 @@ class LlamaCppClient(GenAIClient):
self._supports_audio = info["supports_audio"]
self._supports_tools = info["supports_tools"]
self._supports_reasoning = info["supports_reasoning"]
self._media_marker = info["media_marker"]
logger.info(
"llama.cpp model '%s' initialized — context: %s, vision: %s, audio: %s, tools: %s, reasoning: %s",
@@ -212,7 +215,9 @@ class LlamaCppClient(GenAIClient):
`architecture.input_modalities` (text/image/audio) — the primary
source. When proxied through llama-swap, the same entry carries
`status.args` (server launch argv) and, for the loaded model,
`meta.n_ctx`.
`meta.n_ctx`. /props remains the only source for `media_marker`,
which the server randomizes per startup unless LLAMA_MEDIA_MARKER
is set.
"""
info: dict[str, Any] = {
"context_size": None,
@@ -220,6 +225,7 @@ class LlamaCppClient(GenAIClient):
"supports_audio": False,
"supports_tools": False,
"supports_reasoning": False,
"media_marker": "<__media__>",
}
model_entry: dict[str, Any] | None = None
@@ -308,8 +314,16 @@ class LlamaCppClient(GenAIClient):
# in the Jinja chat template itself.
chat_template = props.get("chat_template") or ""
info["supports_reasoning"] = "enable_thinking" in chat_template
media_marker = props.get("media_marker")
if isinstance(media_marker, str) and media_marker:
info["media_marker"] = media_marker
except Exception as e:
logger.warning("Failed to query llama.cpp /props endpoint: %s", e)
logger.warning(
"Failed to query llama.cpp /props endpoint: %s. "
"Image embeddings may fail if the server randomized its media marker.",
e,
)
return info
@@ -460,6 +474,9 @@ class LlamaCppClient(GenAIClient):
def _transcribe_via_chat(self, audio: bytes, language: str | None) -> str | None:
"""Transcribe through /v1/chat/completions, for servers without the
transcriptions route.
The _media_marker / multimodal_data convention is an /embeddings-only
protocol, so no marker-refresh retry is needed here.
"""
prompt = "Transcribe the speech in this audio verbatim. Respond with the transcript only, and with nothing at all if there is no speech."
@@ -777,16 +794,41 @@ class LlamaCppClient(GenAIClient):
)
return result if result else None
def _refresh_media_marker(self) -> bool:
"""Re-fetch /props and update the cached media marker if it changed.
The server randomizes the marker per startup (unless LLAMA_MEDIA_MARKER
is set), so a stale marker indicates a restart. Returns True iff the
marker was updated to a new value — used to gate a one-shot retry of
a failed embeddings request.
"""
if self.provider is None:
return False
try:
props = self._fetch_llama_props(self.provider, self.genai_config.model)
except Exception as e:
logger.warning("Failed to refresh llama.cpp media marker: %s", e)
return False
marker = props.get("media_marker")
if not isinstance(marker, str) or not marker or marker == self._media_marker:
return False
logger.info("llama.cpp media marker changed (server restart); refreshed")
self._media_marker = marker
return True
def embed(
self,
texts: list[str] | None = None,
images: list[bytes] | None = None,
) -> list[np.ndarray]:
"""Generate embeddings via llama.cpp /v1/embeddings endpoint.
"""Generate embeddings via llama.cpp /embeddings endpoint.
Each text or image is one entry in `input`, using the chat-style
content array from ggml-org/llama.cpp#29556. Server must be started
with --embeddings, and --mmproj for image support.
Supports batch requests. Uses content format with prompt_string and
multimodal_data for images (PR #15108). Server must be started with
--embeddings and --mmproj for multimodal support.
"""
if self.provider is None:
logger.warning(
@@ -801,42 +843,49 @@ class LlamaCppClient(GenAIClient):
EMBEDDING_DIM = 768
inputs: list[dict[str, Any]] = [
{"content": [{"type": "text", "text": text}]} for text in texts
]
encoded_images: list[str] = []
for img in images:
# llama.cpp uses STB which does not support WebP; convert to JPEG
jpeg_bytes = _to_jpeg(img)
to_encode = jpeg_bytes if jpeg_bytes is not None else img
encoded = base64.b64encode(to_encode).decode("utf-8")
# The trailing newline keeps tokenization identical to the older
# "<__media__>\n" prompt_string format, so indexed vectors stay valid
inputs.append(
{
"content": [
{
"type": "image_url",
"image_url": {"url": f"data:image/jpeg;base64,{encoded}"},
},
{"type": "text", "text": "\n"},
]
}
encoded_images.append(base64.b64encode(to_encode).decode("utf-8"))
def build_content() -> list[dict[str, Any]]:
# prompt_string must contain the server's media marker placeholder
# for each image. The marker is randomized per server startup.
content: list[dict[str, Any]] = []
for text in texts:
content.append({"prompt_string": text})
for encoded in encoded_images:
content.append(
{
"prompt_string": f"{self._media_marker}\n",
"multimodal_data": [encoded],
}
)
return content
def post_embeddings() -> requests.Response:
return self._post(
f"{self.provider}/embeddings",
json={"model": self.genai_config.model, "content": build_content()},
timeout=self.timeout,
)
try:
response = self._post(
f"{self.provider}/v1/embeddings",
json={
"model": self.genai_config.model,
"input": inputs,
"encoding_format": "float",
},
timeout=self.timeout,
)
response.raise_for_status()
items = response.json().get("data")
try:
response = post_embeddings()
response.raise_for_status()
except requests.exceptions.RequestException:
# The server may have restarted with a new media marker.
# Refresh from /props; only retry if the marker actually changed.
if not encoded_images or not self._refresh_media_marker():
raise
response = post_embeddings()
response.raise_for_status()
result = response.json()
items = result.get("data", result) if isinstance(result, dict) else result
if not isinstance(items, list):
logger.warning("llama.cpp embeddings returned unexpected format")
return []
@@ -847,7 +896,11 @@ class LlamaCppClient(GenAIClient):
if emb is None:
logger.warning("llama.cpp embeddings item missing embedding field")
continue
arr = np.array(emb, dtype=np.float32).flatten()
arr = np.array(emb, dtype=np.float32)
if arr.ndim > 1:
# llama.cpp can return token-level embeddings; pool per item
arr = arr.mean(axis=0)
arr = arr.flatten()
orig_dim = arr.size
if orig_dim != EMBEDDING_DIM:
if orig_dim > EMBEDDING_DIM:
+2 -28
View File
@@ -104,21 +104,6 @@ def build_review_description_prompt(
else:
return "\n- (No objects detected)"
def get_state_changes_section() -> str:
# empty when nothing changed so the prompt is otherwise unaffected
changes = review_data.get("classification_state_changes")
if not changes:
return ""
return (
"\n\n## State Changes\n\n"
"The camera's state classifiers watch fixed areas of the scene and "
"reported these changes. They come from the classifiers rather than "
"from the images, and they are reliable. Describe each one where it "
"fits in the sequence of events.\n- " + "\n- ".join(changes)
)
fields = get_review_field_guidelines(response_style)
frame_guidance = f"\n{FRAME_ANNOTATION_GUIDANCE}" if frame_captions else ""
@@ -160,7 +145,7 @@ Respond with a JSON object matching the provided schema. Field-specific guidance
- Camera: {review_data["camera"]}
- Total frames: {len(thumbnails)} (Frame 1 = earliest, Frame {len(thumbnails)} = latest){frame_guidance}
- Activity started at {review_data["start"]} and lasted {review_data["duration"]} seconds
- Zones involved: {", ".join(review_data["zones"]) if review_data["zones"] else "None"}{get_state_changes_section()}
- Zones involved: {", ".join(review_data["zones"]) if review_data["zones"] else "None"}
## Objects in Scene
@@ -211,17 +196,6 @@ def build_review_summary_prompt(
f" to "
f"{datetime.datetime.fromtimestamp(end_ts).strftime('%B %d, %Y at %I:%M %p')}"
)
has_state_changes = any(
"state_changes" in item
for event in events
for item in [event, *event.get("context", [])]
)
state_changes_format = (
'\n- "state_changes" (only on some events): changes to monitored areas '
"reported by the camera's state classifiers, which are reliable"
if has_state_changes
else ""
)
prompt = f"""
You are a security officer writing a concise security report.
@@ -229,7 +203,7 @@ Time range: {time_range}
Input format: Each event is a JSON object with:
- "title", "scene", "confidence", "potential_threat_level" (0-2), "other_concerns", "camera", "time", "start_time", "end_time"
- "context": array of related events from other cameras that occurred during overlapping time periods{state_changes_format}
- "context": array of related events from other cameras that occurred during overlapping time periods
**Note: Use the "scene" field for event descriptions in the report. Ignore any "shortSummary" field if present.**
+22 -90
View File
@@ -42,8 +42,6 @@ from frigate.util.ownership import chown_to_runtime
from frigate.util.recording_coverage import (
build_spans,
known_video_codecs,
null_audio_glitches,
realized_timeline,
resolve_coverage,
stream_media_summary,
)
@@ -86,20 +84,10 @@ class StreamRun:
@dataclass
class _ChapterWindow:
"""A merged-timeline slice, shaped like the recording rows chapters read.
lead_in is the output time the slice's vod clip plays before start_time,
from snapping its first frame back to a keyframe.
"""
"""A merged-timeline slice, shaped like the recording rows chapters read."""
start_time: float
end_time: float
lead_in: float = 0.0
def _lead_in(recording: Any) -> float:
"""Output seconds a chapter source plays before its first wall second."""
return recording.lead_in if isinstance(recording, _ChapterWindow) else 0.0
# Matches the setpts factor used in timelapse exports (e.g. setpts=0.04*PTS).
@@ -388,18 +376,15 @@ class RecordingExporter(threading.Thread):
def _resolve_coverage(self) -> tuple[list[list[Any]], set[str], bool]:
"""Resolve the export range into the spans the VOD manifest will serve.
Delegates to the same coverage resolution and glitch nulling the
manifest builder uses, so what we plan around and what nginx-vod
emits agree by construction. Returns the spans (each [row, start,
end, is_main]), the known video codecs, and whether audio survives
the range.
Delegates to the same coverage resolution the manifest builder
uses, so what we plan around and what nginx-vod emits agree by
construction. Returns the spans (each [row, start, end, is_main]),
the known video codecs, and whether audio survives the range.
Memoized: several stages of the export ask the same question, and
the recordings backing a finished range do not change under us.
"""
if self._coverage is None:
intervals = null_audio_glitches(
resolve_coverage(self.camera, self.start_time, self.end_time)
)
intervals = resolve_coverage(self.camera, self.start_time, self.end_time)
self._coverage = (
build_spans(intervals, self.pinned_stream),
known_video_codecs(intervals),
@@ -448,57 +433,17 @@ class RecordingExporter(threading.Thread):
# hand-off to stage around
return True
_spans, codecs, keep_audio = self._resolve_coverage()
runs = self._planned_stream_runs()
spans, codecs, keep_audio = self._resolve_coverage()
runs = self._stream_runs(spans)
# a range one stream covers end to end has nothing to hand off,
# so it stays on the existing path however long it is
if len(runs) < 2:
return True
runs = [piece for run in runs for piece in self._split_long_run(run)]
return self._stage_stream_runs(runs, codecs, keep_audio)
def _planned_stream_runs(self) -> list[StreamRun]:
"""The runs a mixed range is staged as, one pinned vod playlist each."""
runs = self._stream_runs(self._merged_spans())
if len(runs) < 2:
return runs
return [piece for run in runs for piece in self._split_long_run(run)]
def _staged_chapter_windows(self) -> list[_ChapterWindow]:
"""Chapter windows for the staged files as they were rendered.
Each staged run comes from its own pinned vod playlist, whose first
clip snaps back to the preceding keyframe, so a staged file runs up
to a GOP longer than its slice of the merged timeline. Planning each
run the way its playlist does carries that lead-in into the chapter
offsets instead of letting it accumulate at every hand-off.
"""
windows: list[_ChapterWindow] = []
for run in self._planned_stream_runs():
intervals = null_audio_glitches(
resolve_coverage(self.camera, run.start_time, run.end_time)
)
for clip in realized_timeline(intervals, run.stream_type):
# a skipped clip is absent from the playlist and the file
if clip["duration"] <= 0:
continue
span = clip["end_time"] - clip["start_time"]
windows.append(
_ChapterWindow(
clip["start_time"],
clip["end_time"],
max(0.0, clip["duration"] / 1000 - span),
)
)
return windows
def _stream_runs(self, spans: list[list[Any]]) -> list[StreamRun]:
"""Collapse the merged spans into contiguous runs of one stream type.
@@ -895,8 +840,6 @@ class RecordingExporter(threading.Thread):
clipped_end = min(float(rec.end_time), float(self.end_time))
if clipped_end <= clipped_start:
continue
# a staged window's keyframe lead-in plays before it
output_offset += _lead_in(rec)
windows.append((clipped_start, clipped_end, output_offset))
output_offset += clipped_end - clipped_start
@@ -1044,13 +987,9 @@ class RecordingExporter(threading.Thread):
if duration_ms <= 0:
continue
# a staged window's keyframe lead-in opens its chapter, with
# frames captured that long before the window
lead_in = _lead_in(rec)
duration_ms += int(round(lead_in * 1000))
title = datetime.datetime.fromtimestamp(
clipped_start - lead_in, tz=tz
).isoformat(timespec="seconds")
title = datetime.datetime.fromtimestamp(clipped_start, tz=tz).isoformat(
timespec="seconds"
)
chapter_blocks.append(
"[CHAPTER]\n"
"TIMEBASE=1/1000\n"
@@ -1189,12 +1128,12 @@ class RecordingExporter(threading.Thread):
if self.staged_runs:
# each run was already rendered to a temp file with a common
# track timescale, so the concat demuxer has nothing left to
# reconcile
recordings = (
self._staged_chapter_windows()
if self.chapters not in (None, ChaptersEnum.none)
else []
)
# reconcile and every chapter offset lines up with the merged
# timeline the staged files reproduce
recordings = [
_ChapterWindow(span_start, span_end)
for _row, span_start, span_end, _is_main in self._merged_spans()
]
playlist_lines: list[str] = [f"file '{path}'" for path in self.staged_runs]
ffmpeg_input = (
"-y -protocol_whitelist pipe,file -f concat -safe 0 -i /dev/stdin"
@@ -1210,18 +1149,11 @@ class RecordingExporter(threading.Thread):
# its own rows are the ones the chapters describe
recordings = self._get_recordings_for_range(pin)
else:
# an unstaged auto range resolves to at most one stream run, and
# its rows are the ones the chapters describe. Main rows the
# manifest drops (glitches, slivers at the edges of a sub range)
# must not stand in for it.
runs = self._stream_runs(self._merged_spans())
recordings = self._get_recordings_for_range(
runs[0].stream_type if runs else STREAM_TYPE_MAIN
)
# never mix streams in one playlist; use main when available
# and fall back to sub for expired-main history
recordings = self._get_recordings_for_range(STREAM_TYPE_MAIN)
# never mix streams in one playlist; fall back to sub for
# expired-main history
if not recordings and not runs:
if not recordings:
recordings = self._get_recordings_for_range(STREAM_TYPE_SUB)
playlist_lines = []
+4 -42
View File
@@ -490,17 +490,9 @@ class RecordingMaintainer(threading.Thread):
)
reviews = reviews_by_camera[camera]
# probes run concurrently, but each segment's start chains off the
# previous segment's end, so starts resolve in segment order
previous: asyncio.Event | None = None
for recording in recordings:
resolved = asyncio.Event()
tasks.append(
self._validate_in_order(
camera, reviews, recording, previous, resolved
)
)
previous = resolved
tasks.extend(
[self.validate_and_move_segment(camera, reviews, r) for r in recordings]
)
# publish most recently available recording time and None if disabled
if stream_type == STREAM_TYPE_MAIN:
@@ -558,33 +550,12 @@ class RecordingMaintainer(threading.Thread):
while info and info[0][0] < expire_before:
info.pop(0)
async def _validate_in_order(
self,
camera: str,
reviews: Any,
recording: dict[str, Any],
previous_start: asyncio.Event | None,
start_resolved: asyncio.Event,
) -> dict[str, Any] | None:
"""Validate a segment, always releasing the next one in its stream."""
try:
return await self.validate_and_move_segment(
camera, reviews, recording, previous_start, start_resolved
)
finally:
start_resolved.set()
def drop_segment(self, cache_path: str) -> None:
Path(cache_path).unlink(missing_ok=True)
self.end_time_cache.pop(cache_path, None)
async def validate_and_move_segment(
self,
camera: str,
reviews: Any,
recording: dict[str, Any],
previous_start: asyncio.Event | None = None,
start_resolved: asyncio.Event | None = None,
self, camera: str, reviews: Any, recording: dict[str, Any]
) -> dict[str, Any] | None:
cache_path: str = recording["cache_path"]
start_time: datetime.datetime = recording["start_time"]
@@ -646,9 +617,6 @@ class RecordingMaintainer(threading.Thread):
async with self.probe_semaphore:
keyframes = await get_keyframe_offsets(cache_path)
if previous_start is not None:
await previous_start.wait()
start_time = self._resolve_segment_start(
camera, stream_type, start_time, duration, cache_path
)
@@ -686,11 +654,6 @@ class RecordingMaintainer(threading.Thread):
RecordingsDataTypeEnum.valid.value,
)
# the start is settled, so the next segment of the stream can chain
# off it while this one waits on retention and the move
if start_resolved is not None:
start_resolved.set()
record_config = self.config.cameras[camera].record
# sub's alerts/detections carry the retain mode directly, unlike
@@ -1127,7 +1090,6 @@ class RecordingMaintainer(threading.Thread):
elif (
topic == DetectionTypeEnum.api.value
or topic == DetectionTypeEnum.lpr.value
or topic == DetectionTypeEnum.classification_state.value
):
continue
+12 -69
View File
@@ -40,10 +40,6 @@ logger = logging.getLogger(__name__)
THUMB_HEIGHT = 180
THUMB_WIDTH = 320
# seconds before a review item starts that a state classification change is
# still attached to it, e.g. a garage door opening before the car is visible
CLASSIFICATION_STATE_PRE_ROLL = 5
class PendingReviewSegment:
def __init__(
@@ -65,7 +61,6 @@ class PendingReviewSegment:
self.sub_labels = sub_labels
self.zones = zones
self.audio = audio
self.classification_state_changes: list[dict[str, Any]] = []
self.thumb_time: float | None = None
self.last_alert_time: float | None = None
self.last_detection_time: float = frame_time
@@ -167,7 +162,6 @@ class PendingReviewSegment:
"sub_labels": list(self.sub_labels.values()),
"zones": self.zones,
"audio": list(self.audio),
"classification_state_changes": self.classification_state_changes,
"thumb_time": self.thumb_time,
"metadata": None,
},
@@ -299,9 +293,6 @@ class ReviewSegmentMaintainer(threading.Thread):
# manual events
self.indefinite_events: dict[str, dict[str, Any]] = {}
# state classification changes seen while a camera had no review item
self.recent_classification_state_changes: dict[str, list[dict[str, Any]]] = {}
# ensure dirs
Path(os.path.join(CLIPS_DIR, "review")).mkdir(exist_ok=True)
@@ -383,43 +374,6 @@ class ReviewSegmentMaintainer(threading.Thread):
self.active_review_segments[segment.camera] = None
return end_time
def _activate_segment(self, segment: PendingReviewSegment) -> None:
"""Make a segment the camera's active one, attaching any state
classification changes seen just before it started."""
self.active_review_segments[segment.camera] = segment
recent = self.recent_classification_state_changes.pop(segment.camera, [])
segment.classification_state_changes.extend(
c
for c in recent
if c["timestamp"] >= segment.start_time - CLASSIFICATION_STATE_PRE_ROLL
)
def handle_classification_state_change(
self, camera: str, change: dict[str, Any]
) -> None:
"""Attach a verified state classification change to the active segment.
State changes never start, extend, or upgrade a segment. A change seen
with no active segment is held briefly for a segment starting right
after it.
"""
segment = self.active_review_segments.get(camera)
if segment is None:
cutoff = change["timestamp"] - CLASSIFICATION_STATE_PRE_ROLL
self.recent_classification_state_changes[camera] = [
c
for c in self.recent_classification_state_changes.get(camera, [])
if c["timestamp"] >= cutoff
] + [change]
return
prev_data = segment.get_data(False)
segment.classification_state_changes.append(change)
self._publish_segment_update(
segment, self.config.cameras[camera], None, [], prev_data
)
def forcibly_end_segment(self, camera: str) -> Any:
"""Forcibly end the pending segment for a camera."""
segment = self.active_review_segments.get(camera)
@@ -468,7 +422,6 @@ class ReviewSegmentMaintainer(threading.Thread):
"""Close out a deleted camera's segment so a reused name cannot inherit it."""
self.forcibly_end_segment(camera)
self.indefinite_events.pop(camera, None)
self.recent_classification_state_changes.pop(camera, None)
def update_existing_segment(
self,
@@ -626,7 +579,7 @@ class ReviewSegmentMaintainer(threading.Thread):
audio=set(),
zones=list(new_zones),
)
self._activate_segment(new_segment)
self.active_review_segments[segment.camera] = new_segment
self._publish_segment_start(new_segment)
new_segment.last_detection_time = last_detection_time
elif segment.severity == SeverityEnum.detection and frame_time > (
@@ -686,7 +639,7 @@ class ReviewSegmentMaintainer(threading.Thread):
audio=set(),
zones=zones,
)
self._activate_segment(new_segment)
self.active_review_segments[camera] = new_segment
try:
yuv_frame = self.frame_manager.get(
@@ -760,10 +713,6 @@ class ReviewSegmentMaintainer(threading.Thread):
if camera not in self.indefinite_events:
self.indefinite_events[camera] = {}
elif topic == DetectionTypeEnum.classification_state.value:
(camera, classification_change) = data
else:
continue
if camera not in self.config.cameras:
continue
@@ -774,10 +723,6 @@ class ReviewSegmentMaintainer(threading.Thread):
):
continue
if topic == DetectionTypeEnum.classification_state:
self.handle_classification_state_change(camera, classification_change)
continue
current_segment = self.active_review_segments.get(camera)
# Check if the current segment should be processed based on enabled settings
@@ -919,16 +864,14 @@ class ReviewSegmentMaintainer(threading.Thread):
severity = SeverityEnum.detection
if severity:
self._activate_segment(
PendingReviewSegment(
camera,
frame_time,
severity,
{},
{},
[],
detections,
)
self.active_review_segments[camera] = PendingReviewSegment(
camera,
frame_time,
severity,
{},
{},
[],
detections,
)
elif topic == DetectionTypeEnum.api:
severity = self.get_manual_event_severity(
@@ -945,7 +888,7 @@ class ReviewSegmentMaintainer(threading.Thread):
[],
set(),
)
self._activate_segment(api_segment)
self.active_review_segments[camera] = api_segment
if manual_info["state"] == ManualEventState.start:
self.indefinite_events[camera][manual_info["event_id"]] = (
@@ -972,7 +915,7 @@ class ReviewSegmentMaintainer(threading.Thread):
[],
set(),
)
self._activate_segment(lpr_segment)
self.active_review_segments[camera] = lpr_segment
if manual_info["state"] == ManualEventState.start:
self.indefinite_events[camera][manual_info["event_id"]] = (
-11
View File
@@ -256,17 +256,6 @@ class HardwareStats:
)
self.update_config()
def set_config(self, config: FrigateConfig) -> None:
"""Follow a runtime config swap and recalculate the monitored hardware.
The camera update subscriber has to follow too, or later camera updates
would land on the discarded config.
"""
self.config = config
self._config_subscriber.config = config
self._config_subscriber.camera_configs = config.cameras
self.update_config()
def update_config(self) -> None:
"""Recalculate all hardware that needs to be monitored from the config."""
names = self._scan_ffmpeg() | self._scan_detectors() | self._scan_enrichments()
+4 -7
View File
@@ -111,18 +111,15 @@ def get_detector_stats(
) -> dict[str, dict[str, Any]]:
"""Get stats for all detectors, including temperatures based on detector type."""
detector_stats: dict[str, dict[str, Any]] = {}
# detector type -> device -> index into that type's temperatures
device_indices: dict[str, dict[str, int]] = {}
detector_type_indices: dict[str, int] = {}
for name, detector in stats_tracking["detectors"].items():
pid = detector.detect_process.pid if detector.detect_process else None
detector_type = detector.detector_config.type
# temperatures are per physical unit, so a repeated device
# ("hailo:PCIe#2", see runner_names) shares its unit's reading
device = name.partition("#")[0]
type_devices = device_indices.setdefault(detector_type, {})
current_index = type_devices.setdefault(device, len(type_devices))
# Keep track of the index for each detector type to match temperatures correctly
current_index = detector_type_indices.get(detector_type, 0)
detector_type_indices[detector_type] = current_index + 1
detector_stat = {
"inference_speed": round(detector.avg_inference_speed.value * 1000, 2), # type: ignore[attr-defined]
+1 -41
View File
@@ -1,10 +1,8 @@
import json
import os
from unittest.mock import Mock, patch
import frigate.genai
from frigate.config import GenAIProviderEnum
from frigate.const import MODEL_CACHE_DIR, REDACTED_CREDENTIAL_SENTINEL
from frigate.const import REDACTED_CREDENTIAL_SENTINEL
from frigate.genai import GenAIClient
from frigate.models import Event, Recordings, ReviewSegment
from frigate.stats.emitter import StatsEmitter
@@ -92,44 +90,6 @@ class TestHttpApp(BaseTestHttp):
mqtt = response.json()["mqtt"]
assert mqtt["password"] == REDACTED_CREDENTIAL_SENTINEL
def test_config_response_keeps_plus_model_reference(self):
model_id = "test_plus_reference"
model_path = os.path.join(MODEL_CACHE_DIR, model_id)
os.makedirs(MODEL_CACHE_DIR, exist_ok=True)
with open(model_path, "w") as f:
f.write("model")
with open(f"{model_path}.json", "w") as f:
json.dump(
{
"id": model_id,
"type": "ssd",
"supportedDetectors": ["cpu"],
"width": 320,
"height": 320,
"inputShape": "nhwc",
"pixelFormat": "rgb",
"labelMap": {"0": "person"},
},
f,
)
self.addCleanup(os.remove, model_path)
self.addCleanup(os.remove, f"{model_path}.json")
self.minimal_config["models"] = [
{"path": f"plus://{model_id}", "devices": ["cpu"]}
]
app = super().create_app()
with AuthTestClient(app) as client:
response = client.get("/config")
assert response.status_code == 200
assert response.json()["models"][0]["path"] == f"plus://{model_id}"
# detection still loads the resolved cache file
assert app.frigate_config.models[0].path == model_path
####################################################################################################################
################################### POST /genai/probe Endpoint ##################################################
####################################################################################################################
-4
View File
@@ -179,16 +179,12 @@ class TestReviewMaintainerRemoval(unittest.TestCase):
maintainer = ReviewSegmentMaintainer.__new__(ReviewSegmentMaintainer)
maintainer.active_review_segments = {"deleted_cam": MagicMock()}
maintainer.indefinite_events = {"deleted_cam": {"1234.5-abcdef": 1.0}}
maintainer.recent_classification_state_changes = {
"deleted_cam": [{"model": "gate", "from": "a", "to": "b", "timestamp": 1.0}]
}
maintainer.forcibly_end_segment = MagicMock()
maintainer._handle_camera_removed("deleted_cam")
maintainer.forcibly_end_segment.assert_called_once_with("deleted_cam")
self.assertNotIn("deleted_cam", maintainer.indefinite_events)
self.assertNotIn("deleted_cam", maintainer.recent_classification_state_changes)
class TestAutotrackerMoveQueue(unittest.TestCase):
-1
View File
@@ -26,7 +26,6 @@ class TestSwapRuntimeConfig(unittest.TestCase):
app.genai_manager.update_config.assert_called_once_with(config)
app.profile_manager.update_config.assert_called_once_with(config)
self.assertIs(app.stats_emitter.config, config)
app.stats_emitter.hardware_stats.set_config.assert_called_once_with(config)
self.assertIs(app.dispatcher.config, config)
for comm in app.dispatcher.comms:
self.assertIs(comm.config, config)
-39
View File
@@ -1,39 +0,0 @@
"""Tests for per-detector stats."""
import unittest
from unittest.mock import MagicMock, patch
from frigate.stats.util import get_detector_stats
def _detector(detector_type: str) -> MagicMock:
detector = MagicMock()
detector.detector_config.type = detector_type
detector.avg_inference_speed.value = 0.01
detector.detection_start.value = 0.0
detector.detect_process.pid = 1
return detector
class TestDetectorTemperatures(unittest.TestCase):
def test_repeated_device_shares_its_unit_temperature(self):
stats_tracking = {
"detectors": {
"hailo:PCIe": _detector("hailo8l"),
"hailo:PCIe#2": _detector("hailo8l"),
"hailo:PCIe:1": _detector("hailo8l"),
}
}
with patch(
"frigate.stats.util.get_hardware_temperatures", return_value=[50.0, 60.0]
):
stats = get_detector_stats(stats_tracking)
self.assertEqual(stats["hailo:PCIe"]["temperature"], 50.0)
self.assertEqual(stats["hailo:PCIe#2"]["temperature"], 50.0)
self.assertEqual(stats["hailo:PCIe:1"]["temperature"], 60.0)
if __name__ == "__main__":
unittest.main()
-50
View File
@@ -543,56 +543,6 @@ class TestPinnedStream(unittest.TestCase):
self.assertFalse(any("/vod/front/main/" in token for token in cmd))
class TestExportTimelineAlignment(unittest.TestCase):
def test_unstaged_auto_reads_the_stream_it_serves(self) -> None:
# a sub-only range whose main rows are glitches the manifest drops
exporter = _make_exporter([_span("/s1.mp4", 1_000, 1_040, False)], {"h264"})
streams: list[str] = []
def rows(stream: str) -> list:
streams.append(stream)
return [_FakeRow(f"/{stream}.mp4")]
exporter._get_recordings_for_range = rows # type: ignore[method-assign]
exporter.get_record_export_command("/exports/out.mp4")
self.assertEqual(streams, ["sub"])
def test_staged_chapters_carry_keyframe_lead_in(self) -> None:
exporter = _make_exporter(
[
_span("/m1.mp4", 1_000, 1_020, True),
_span("/s1.mp4", 1_020, 1_040, False),
],
{"h264"},
)
exporter.config.ui.timezone = None
# each run's vod clip snaps 1.5s back to a keyframe
def timeline(_intervals: list, stream: str) -> list[dict]:
start, end = (1_000, 1_020) if stream == "main" else (1_020, 1_040)
return [
{
"start_time": start,
"end_time": end,
"duration": (end - start + 1.5) * 1000,
}
]
with (
patch("frigate.record.export.resolve_coverage", return_value=[]),
patch("frigate.record.export.realized_timeline", side_effect=timeline),
):
windows = exporter._staged_chapter_windows()
path = exporter._build_recording_segment_chapter_metadata_file(windows)
self.addCleanup(os.remove, path)
content = Path(path).read_text()
self.assertIn("START=0\nEND=21500", content)
self.assertIn("START=21500\nEND=43000", content)
class TestStagedFileCleanup(unittest.TestCase):
"""A staged path must be tracked before ffmpeg can write to it."""
-45
View File
@@ -7,7 +7,6 @@ from frigate.genai.prompts import (
REVIEW_DESCRIPTION_FIELD_GUIDELINES,
REVIEW_RESPONSE_STYLES,
build_review_description_prompt,
build_review_summary_prompt,
get_review_field_guidelines,
)
@@ -84,49 +83,5 @@ class TestReviewResponseStyle(unittest.TestCase):
)
class TestClassificationStateChanges(unittest.TestCase):
def _build_prompt(self, **extra) -> str:
review_data = {
"camera": "Front Door",
"start": "Monday, 09:30 AM",
"duration": 25,
"zones": [],
"unified_objects": ["person"],
**extra,
}
return build_review_description_prompt(
review_data, [b"fake-image"], [], None, "activity context"
)
def test_no_changes_leaves_prompt_unchanged(self):
self.assertEqual(
self._build_prompt(classification_state_changes=[]),
self._build_prompt(),
)
self.assertNotIn("## State Changes", self._build_prompt())
def test_changes_are_listed_before_objects(self):
change = "front gate changed from closed to open, 12s into the activity"
prompt = self._build_prompt(classification_state_changes=[change])
self.assertIn(f"\n- {change}\n\n## Objects in Scene", prompt)
self.assertLess(
prompt.index("## Sequence Details"), prompt.index("## State Changes")
)
def test_summary_describes_state_changes_only_when_present(self):
event = {"title": "Person at door", "camera": "Front Door", "context": []}
without = build_review_summary_prompt(0, 3600, [event], None)
self.assertNotIn('"state_changes"', without)
context_event = {
**event,
"context": [
{"camera": "Driveway", "state_changes": ["gate changed from a to b"]}
],
}
with_changes = build_review_summary_prompt(0, 3600, [context_event], None)
self.assertIn('- "state_changes"', with_changes)
if __name__ == "__main__":
unittest.main()
+2 -58
View File
@@ -19,8 +19,6 @@ import unittest
from types import SimpleNamespace
from unittest.mock import AsyncMock, MagicMock, patch
import requests
from frigate.config import GenAIConfig, GenAIProviderEnum
from frigate.genai import PROVIDERS, load_providers
@@ -568,6 +566,7 @@ class TestLlamaCppProvider(unittest.TestCase):
"supports_audio": False,
"supports_tools": False,
"supports_reasoning": False,
"media_marker": "<__media__>",
}
cls = PROVIDERS[GenAIProviderEnum.llamacpp]
with patch.object(cls, "_get_model_info", return_value=info):
@@ -590,62 +589,6 @@ class TestLlamaCppProvider(unittest.TestCase):
with patch.object(client, "_fetch_models_data", return_value=models_data):
self.assertEqual(client.list_models(), ["g4", "gemma", "qwen3-asr"])
@staticmethod
def _embeddings_response(vectors):
response = MagicMock()
response.status_code = 200
response.json.return_value = {
"object": "list",
"data": [
{"object": "embedding", "index": i, "embedding": v}
for i, v in enumerate(vectors)
],
}
return response
def test_embed_posts_content_arrays_to_v1_embeddings(self):
client = self._client()
response = self._embeddings_response([[0.1] * 768, [0.2] * 768])
with patch.object(client, "_post", return_value=response) as post:
result = client.embed(texts=["a person"], images=[b"not an image"])
url = post.call_args.args[0]
payload = post.call_args.kwargs["json"]
self.assertEqual(url, "http://localhost:9999/v1/embeddings")
self.assertEqual(payload["model"], "m")
self.assertEqual(payload["encoding_format"], "float")
self.assertEqual(
payload["input"][0], {"content": [{"type": "text", "text": "a person"}]}
)
image_parts = payload["input"][1]["content"]
self.assertEqual(image_parts[0]["type"], "image_url")
self.assertEqual(
image_parts[0]["image_url"]["url"],
"data:image/jpeg;base64," + base64.b64encode(b"not an image").decode(),
)
self.assertEqual(image_parts[1], {"type": "text", "text": "\n"})
self.assertEqual(len(result), 2)
self.assertAlmostEqual(float(result[1][0]), 0.2, places=5)
def test_embed_normalizes_dimension(self):
client = self._client()
response = self._embeddings_response([[1.0] * 1024, [1.0] * 512])
with patch.object(client, "_post", return_value=response):
result = client.embed(texts=["long", "short"])
self.assertEqual([r.shape for r in result], [(768,), (768,)])
self.assertEqual(float(result[1][-1]), 0.0)
def test_embed_request_error_returns_empty(self):
client = self._client()
response = MagicMock()
response.raise_for_status.side_effect = requests.exceptions.HTTPError("400")
with patch.object(client, "_post", return_value=response):
self.assertEqual(client.embed(texts=["a"]), [])
# ---------------------------------------------------------------------------
# transcribe role
@@ -819,6 +762,7 @@ class TestLlamaCppTranscribe(unittest.TestCase):
"supports_audio": supports_audio,
"supports_tools": False,
"supports_reasoning": False,
"media_marker": "<__media__>",
}
cls = PROVIDERS[GenAIProviderEnum.llamacpp]
with patch.object(cls, "_get_model_info", return_value=info):
-11
View File
@@ -288,17 +288,6 @@ class TestUpdateConfig(HardwareStatsTestCase):
self.assertEqual(set(stats._monitored), {"rockchip"})
def test_follows_a_runtime_config_swap(self):
stats = self.make_stats(self.make_config())
self.assertEqual(set(stats._monitored), set())
swapped = self.make_config("preset-rk-h264")
stats.set_config(swapped)
self.assertEqual(set(stats._monitored), {"rockchip"})
self.assertIs(self.subscriber.return_value.config, swapped)
self.assertIs(self.subscriber.return_value.camera_configs, swapped.cameras)
class TestUpdateStats(HardwareStatsTestCase):
def run_stats(self, stats: HardwareStats) -> dict:
+3 -6
View File
@@ -1,7 +1,7 @@
import datetime
import sys
import unittest
from unittest.mock import AsyncMock, MagicMock, patch
from unittest.mock import MagicMock, patch
# Mock complex imports before importing maintainer, saving originals so we can
# restore them after import and avoid polluting sys.modules for other tests.
@@ -48,11 +48,8 @@ class TestMaintainer(unittest.IsolatedAsyncioTestCase):
"frigate.record.maintainer.psutil.process_iter", return_value=[]
):
with patch("frigate.record.maintainer.logger.warning") as warn:
# Mock validate_and_move_segment to avoid further logic.
# The requestor is real when another test imported the
# maintainer first, and it would block on a reply.
maintainer.validate_and_move_segment = AsyncMock()
maintainer.requestor = MagicMock()
# Mock validate_and_move_segment to avoid further logic
maintainer.validate_and_move_segment = MagicMock()
try:
await maintainer.move_files()
@@ -1,6 +1,5 @@
"""Tests for sub stream cache segment handling in the recording maintainer."""
import asyncio
import datetime
import os
import tempfile
@@ -533,59 +532,6 @@ class TestSegmentStartChaining(unittest.IsolatedAsyncioTestCase):
self.assertAlmostEqual(calls[1].args[2].timestamp(), self.T0 + 10.4, places=3)
self.assertAlmostEqual(calls[1].args[3].timestamp(), self.T0 + 20.8, places=3)
async def test_out_of_order_probes_chain_in_segment_order(self):
maintainer = _build_chaining_maintainer(self.T0)
async def probe(_ffmpeg, cache_path, get_duration=False):
# the earlier segment's probe finishes last
if "chain0" in cache_path:
await asyncio.sleep(0.05)
return {"has_valid_video": True, "duration": 10.4}
recordings = [
{
"start_time": datetime.datetime.fromtimestamp(
self.T0 + offset, tz=datetime.UTC
),
"cache_path": f"/tmp/cache/test_cam@chain{offset}.mp4",
"stream_type": "main",
}
for offset in (0, 10)
]
first_resolved = asyncio.Event()
with (
patch("frigate.record.maintainer.get_video_properties", probe),
patch(
"frigate.record.maintainer.get_keyframe_offsets",
AsyncMock(return_value=[0]),
),
patch(
"frigate.record.maintainer.os.path.getmtime",
MagicMock(side_effect=OSError("missing")),
),
):
await asyncio.gather(
maintainer._validate_in_order(
"test_cam", [], recordings[0], None, first_resolved
),
maintainer._validate_in_order(
"test_cam", [], recordings[1], first_resolved, asyncio.Event()
),
)
starts = sorted(
call.args[2].timestamp() for call in maintainer.move_segment.await_args_list
)
self.assertEqual(starts[0], self.T0)
self.assertAlmostEqual(starts[1], self.T0 + 10.4, places=3)
self.assertAlmostEqual(
maintainer.last_segment_end[("test_cam", "main")],
self.T0 + 20.8,
places=3,
)
async def test_genuine_gap_is_not_snapped(self):
maintainer = _build_chaining_maintainer(self.T0)
-73
View File
@@ -1,13 +1,10 @@
"""Tests for tracker-derived review frame annotations."""
import unittest
from unittest.mock import patch
from frigate.data_processing.post.review_annotations import (
annotations_by_frame,
build_frame_captions,
build_timeline,
describe_classification_change,
describe_heading,
describe_position,
event_name,
@@ -315,75 +312,5 @@ class TestFrameBucketing(unittest.TestCase):
self.assertEqual(annotations_by_frame([(1.0, "x")], []), {})
class TestClassificationChangeCaptions(unittest.TestCase):
def setUp(self):
person = track(
"1789481994.684479-lpyc2z",
"person",
0.0,
straight_path((0.9, 0.6), (0.4, 0.3), 10, 0.0),
)
patcher = patch(
"frigate.data_processing.post.review_annotations.get_tracked_events",
return_value=[person],
)
patcher.start()
self.addCleanup(patcher.stop)
patcher = patch(
"frigate.data_processing.post.review_annotations.get_state_changes",
return_value=[],
)
patcher.start()
self.addCleanup(patcher.stop)
def test_change_is_phrased_without_underscores(self):
self.assertEqual(
describe_classification_change(
{"model": "trash_day", "from": "no_bins", "to": "bins_at_curb"}
),
"trash day changed from no bins to bins at curb",
)
def test_change_is_noted_before_the_frame_it_precedes(self):
change = {"model": "front_gate", "from": "closed", "to": "open"}
captions = build_frame_captions(
["1789481994.684479-lpyc2z"],
[0.0, 10.0, 20.0],
[{**change, "timestamp": 12.0}],
)
self.assertEqual(
captions[2],
"Frame 3 of 3 (+20.0s):\n[state] front gate changed from closed to open",
)
self.assertTrue(captions[0].splitlines()[1].startswith("[tracker] "))
def test_change_is_noted_without_tracked_objects(self):
with patch(
"frigate.data_processing.post.review_annotations.get_tracked_events",
return_value=[],
):
captions = build_frame_captions(
[],
[0.0, 10.0],
[{"model": "gate", "from": "a", "to": "b", "timestamp": 5.0}],
)
self.assertEqual(
captions,
[
"Frame 1 of 2 (+0.0s):",
"Frame 2 of 2 (+10.0s):\n[state] gate changed from a to b",
],
)
def test_change_after_the_last_frame_is_dropped(self):
captions = build_frame_captions(
["1789481994.684479-lpyc2z"],
[0.0, 10.0, 20.0],
[{"model": "gate", "from": "a", "to": "b", "timestamp": 25.0}],
)
self.assertFalse(any("[state]" in caption for caption in captions))
if __name__ == "__main__":
unittest.main()
@@ -1,258 +0,0 @@
"""Tests for attaching state classification changes to review items."""
import unittest
from unittest.mock import MagicMock, patch
from frigate.comms.embeddings_updater import EmbeddingsRequestEnum
from frigate.config import FrigateConfig
from frigate.data_processing.post.review_descriptions import (
ReviewDescriptionProcessor,
format_classification_state_changes,
)
from frigate.data_processing.real_time.custom_classification import (
CustomStateClassificationProcessor,
)
from frigate.models import ReviewSegment
from frigate.review.maintainer import (
CLASSIFICATION_STATE_PRE_ROLL,
PendingReviewSegment,
ReviewSegmentMaintainer,
)
from frigate.review.types import SeverityEnum
CONFIG = """
mqtt:
enabled: False
cameras:
front_door:
ffmpeg:
inputs:
- path: rtsp://10.0.0.1:554/video
roles:
- detect
detect:
width: 1920
height: 1080
fps: 5
"""
def gate_change(timestamp: float, before: str = "closed", after: str = "open"):
return {"model": "front_gate", "from": before, "to": after, "timestamp": timestamp}
class TestVerifyStateChange(unittest.TestCase):
def setUp(self):
self.processor = CustomStateClassificationProcessor.__new__(
CustomStateClassificationProcessor
)
self.processor.state_history = {}
def verify(self, state: str, timestamp: float):
return self.processor.verify_state_change("front_door", state, timestamp)
def test_first_verified_state_has_no_previous_state(self):
self.assertIsNone(self.verify("closed", 1.0))
self.assertIsNone(self.verify("closed", 2.0))
self.assertEqual(self.verify("closed", 3.0), (None, 1.0))
def test_change_reports_previous_state_and_first_sighting(self):
for timestamp in (1.0, 2.0, 3.0):
self.verify("closed", timestamp)
self.assertIsNone(self.verify("open", 10.0))
self.assertIsNone(self.verify("open", 11.0))
self.assertEqual(self.verify("open", 12.0), ("closed", 10.0))
def test_interrupted_verification_restarts_the_first_sighting(self):
for timestamp in (1.0, 2.0, 3.0):
self.verify("closed", timestamp)
self.verify("open", 10.0)
self.verify("closed", 11.0)
self.verify("open", 20.0)
self.verify("open", 21.0)
self.assertEqual(self.verify("open", 22.0), ("closed", 20.0))
def test_reload_forgets_states_the_model_no_longer_has(self):
for timestamp in (1.0, 2.0, 3.0):
self.verify("closed", timestamp)
self.processor.state_history["back_door"] = {"current_state": "open"}
self.processor.labelmap = {0: "open", 1: "shut"}
self.processor._forget_unknown_states()
self.assertEqual(list(self.processor.state_history), ["back_door"])
self.assertIsNone(self.verify("shut", 10.0))
self.assertIsNone(self.verify("shut", 11.0))
self.assertEqual(self.verify("shut", 12.0), (None, 10.0))
class TestReviewSegmentAttachment(unittest.TestCase):
def setUp(self):
self.maintainer = ReviewSegmentMaintainer.__new__(ReviewSegmentMaintainer)
self.maintainer.config = FrigateConfig.parse_yaml(CONFIG)
self.maintainer.active_review_segments = {}
self.maintainer.recent_classification_state_changes = {}
self.maintainer._publish_segment_update = MagicMock()
def segment(self, start_time: float) -> PendingReviewSegment:
return PendingReviewSegment(
"front_door",
start_time,
SeverityEnum.alert,
{"1.0-abcdef": "person"},
{},
[],
set(),
)
def test_change_during_segment_is_attached_and_published(self):
segment = self.segment(100.0)
self.maintainer.active_review_segments["front_door"] = segment
self.maintainer.handle_classification_state_change(
"front_door", gate_change(110.0)
)
self.assertEqual(segment.classification_state_changes, [gate_change(110.0)])
self.maintainer._publish_segment_update.assert_called_once()
prev_data = self.maintainer._publish_segment_update.call_args.args[4]
self.assertEqual(prev_data["data"]["classification_state_changes"], [])
self.assertEqual(
segment.get_data(False)["data"]["classification_state_changes"],
[gate_change(110.0)],
)
def test_change_never_starts_a_segment(self):
self.maintainer.handle_classification_state_change(
"front_door", gate_change(110.0)
)
self.assertIsNone(self.maintainer.active_review_segments.get("front_door"))
self.maintainer._publish_segment_update.assert_not_called()
def test_change_just_before_a_segment_is_attached_when_it_starts(self):
self.maintainer.handle_classification_state_change(
"front_door", gate_change(98.0)
)
segment = self.segment(100.0)
self.maintainer._activate_segment(segment)
self.assertIs(self.maintainer.active_review_segments["front_door"], segment)
self.assertEqual(segment.classification_state_changes, [gate_change(98.0)])
self.assertNotIn(
"front_door", self.maintainer.recent_classification_state_changes
)
def test_change_long_before_a_segment_is_not_attached(self):
self.maintainer.handle_classification_state_change(
"front_door", gate_change(100.0 - CLASSIFICATION_STATE_PRE_ROLL - 1)
)
segment = self.segment(100.0)
self.maintainer._activate_segment(segment)
self.assertEqual(segment.classification_state_changes, [])
def test_held_changes_are_pruned(self):
self.maintainer.handle_classification_state_change(
"front_door", gate_change(10.0)
)
self.maintainer.handle_classification_state_change(
"front_door", gate_change(50.0, "open", "closed")
)
self.assertEqual(
self.maintainer.recent_classification_state_changes["front_door"],
[gate_change(50.0, "open", "closed")],
)
class TestChangeTiming(unittest.TestCase):
def test_changes_are_placed_relative_to_the_activity(self):
lines = format_classification_state_changes(
[
gate_change(98.0),
gate_change(112.4, "open", "closed"),
gate_change(140.0),
],
start_time=100.0,
end_time=130.0,
)
self.assertEqual(
lines,
[
"front gate changed from closed to open, "
"just before the activity started",
"front gate changed from open to closed, 12s into the activity",
"front gate changed from closed to open, after the activity ended",
],
)
class TestSummaryContext(unittest.TestCase):
def row(self, camera, start, end, threat, changes=()):
return {
"camera": camera,
"start_time": start,
"end_time": end,
"data": {
"metadata": {"title": camera, "potential_threat_level": threat},
"classification_state_changes": list(changes),
},
}
def summarize(self, rows):
processor = ReviewDescriptionProcessor.__new__(ReviewDescriptionProcessor)
processor.config = FrigateConfig.parse_yaml(CONFIG)
processor.genai_manager = MagicMock()
client = processor.genai_manager.description_client
with patch.object(ReviewSegment, "select") as select:
query = select.return_value.where.return_value.order_by.return_value
query.dicts.return_value.iterator.return_value = iter(rows)
processor.handle_request(
EmbeddingsRequestEnum.summarize_review.value,
{"start_ts": 0, "end_ts": 100},
)
return client.generate_review_summary.call_args.args[2]
def test_context_state_changes_stay_with_their_review(self):
events = self.summarize(
[
self.row("front_door", 10, 60, 1),
self.row("driveway", 15, 25, 0, [gate_change(20.0)]),
self.row("driveway", 30, 40, 0, [gate_change(35.0, "open", "closed")]),
]
)
self.assertEqual(
[
(item["start_time"], item["end_time"], item["state_changes"])
for item in events[0]["context"]
],
[
(15, 25, ["front gate changed from closed to open"]),
(30, 40, ["front gate changed from open to closed"]),
],
)
def test_later_context_review_without_changes_is_deduplicated(self):
events = self.summarize(
[
self.row("front_door", 10, 60, 1),
self.row("driveway", 15, 25, 0, [gate_change(20.0)]),
self.row("driveway", 30, 40, 0),
]
)
self.assertEqual(len(events[0]["context"]), 1)
self.assertEqual(events[0]["context"][0]["start_time"], 15)
if __name__ == "__main__":
unittest.main()
+1 -7
View File
@@ -14,10 +14,6 @@ from frigate.util.file import FileLock
logger = logging.getLogger(__name__)
# (connect, read) seconds. The read timeout bounds each socket read rather than
# the whole download, so large models still finish.
DOWNLOAD_TIMEOUT = (15, 60)
# target path -> first line of the last download error for it; every existing
# download function swallows its exceptions, so this is how the downloader
# thread learns why a file is still missing
@@ -128,9 +124,7 @@ class ModelDownloader:
logger.info(f"Downloading model file from: {url}")
try:
with requests.get(
url, stream=True, allow_redirects=True, timeout=DOWNLOAD_TIMEOUT
) as r:
with requests.get(url, stream=True, allow_redirects=True) as r:
r.raise_for_status()
with open(temporary_filename, "wb") as f:
for chunk in r.iter_content(chunk_size=8192):
+7 -7
View File
@@ -213,19 +213,17 @@ def stream_has_audio(intervals: list[CoverageInterval], main: bool) -> bool:
)
def null_audio_glitches(intervals: list[CoverageInterval]) -> list[CoverageInterval]:
def null_audio_glitches(
intervals: list[CoverageInterval], main_audio: bool, sub_audio: bool
) -> list[CoverageInterval]:
"""Treat video-only glitch rows on audio-bearing streams as no recording.
nginx-vod requires every clip in a sequence to carry the same track
count, so a truncated video-only segment (a backend restart can flush
a sub-second file before any audio packet landed) poisons every
manifest that includes it. Nulling the row turns the glitch into a
hole the span builder skips like any recording gap. Every consumer of
a window's coverage (the vod manifest, its realized timelines, and
exports) goes through here, so they all agree on which rows exist.
hole the span builder skips like any recording gap.
"""
main_audio = stream_has_audio(intervals, main=True)
sub_audio = stream_has_audio(intervals, main=False)
result: list[CoverageInterval] = []
for interval in intervals:
main = interval.main
@@ -451,7 +449,9 @@ def realized_timelines(
assembles each variant's realized spans. Keyframe snapping reads the
per-row index stored at record time, so no file is touched.
"""
nulled = null_audio_glitches(intervals)
main_audio = stream_has_audio(intervals, main=True)
sub_audio = stream_has_audio(intervals, main=False)
nulled = null_audio_glitches(intervals, main_audio, sub_audio)
return {
"auto": realized_timeline(nulled, None),
-204
View File
@@ -1,204 +0,0 @@
/**
* Helpers for the live dashboard's draggable grid layout: reading and seeding
* the persisted layout, and measuring rendered tiles.
*
* DraggableGridLayout persists through useUserPersistence, which namespaces
* keys by username, and every write is an async idb put. A test that seeds the
* bare key, or seeds before the app's own first write has landed, silently
* asserts against a key the app never reads. persistedLayoutKey() closes both
* holes, so prefer it over building the key by hand.
*
* Geometry has its own trap: the grid first lays out against window.innerWidth,
* then reflows narrower once useResizeObserver reports the real container.
* Tiles measured in separate round-trips can straddle that reflow and disagree
* on scale, so cameraBoxes() takes every measurement in one evaluate.
*
* Used by live-grid-aspect-modes.spec.ts and masonry-live-grid.spec.ts.
*/
import { expect, type Page } from "@playwright/test";
export type LayoutItem = {
i: string;
x: number;
y: number;
w: number;
h: number;
};
export type PersistedLayout = {
version: number;
naturalAspect: boolean;
layout: LayoutItem[];
};
function layoutKeySuffix(group: string): string {
return `${group}-draggable-layout`;
}
/**
* The key the app has actually written an envelope to, or undefined while its
* first write is still in flight.
*/
function findWrittenKey(
page: Page,
group: string,
): Promise<string | undefined> {
return page.evaluate(
(suffix) =>
new Promise<string | undefined>((resolve) => {
const open = indexedDB.open("keyval-store");
open.onsuccess = () => {
const store = open.result
.transaction("keyval", "readonly")
.objectStore("keyval");
// getAllKeys and getAll both return in key order, so the indexes align
const keys = store.getAllKeys();
const values = store.getAll();
keys.transaction.oncomplete = () => {
open.result.close();
const names = keys.result as string[];
const stored = values.result as { version?: number }[];
const match = names.findIndex(
(name, index) =>
(name === suffix || name.startsWith(`${suffix}:`)) &&
typeof stored[index]?.version === "number",
);
resolve(match === -1 ? undefined : names[match]);
};
};
open.onerror = () => resolve(undefined);
}),
layoutKeySuffix(group),
);
}
/**
* Wait for the grid to persist its own layout, then return the key it used.
* Waiting for that write is what makes a later seed meaningful: it proves the
* key is live, and it rules out the app overwriting the seed a moment later.
*/
export async function persistedLayoutKey(
page: Page,
group: string,
): Promise<string> {
let key: string | undefined;
await expect
.poll(async () => (key = await findWrittenKey(page, group)), {
timeout: 10_000,
message: `grid never persisted a layout for group "${group}"`,
})
.not.toBeUndefined();
return key!;
}
/** Overwrite the stored layout, resolving only once the put has committed. */
export function seedLayout(
page: Page,
key: string,
value: unknown,
): Promise<void> {
return page.evaluate(
([key, value]) =>
new Promise<void>((resolve, reject) => {
const open = indexedDB.open("keyval-store");
open.onupgradeneeded = () => open.result.createObjectStore("keyval");
open.onsuccess = () => {
const tx = open.result.transaction("keyval", "readwrite");
tx.objectStore("keyval").put(value, key as string);
tx.oncomplete = () => {
open.result.close();
resolve();
};
tx.onerror = () => reject(tx.error);
};
open.onerror = () => reject(open.error);
}),
[key, value] as const,
);
}
/** Read the stored layout back. Undefined until the app writes it. */
export function readLayout(
page: Page,
key: string,
): Promise<PersistedLayout | undefined> {
return page.evaluate(
(target) =>
new Promise((resolve) => {
const open = indexedDB.open("keyval-store");
open.onsuccess = () => {
const tx = open.result.transaction("keyval", "readonly");
const request = tx.objectStore("keyval").get(target);
tx.oncomplete = () => {
open.result.close();
resolve(request.result);
};
};
open.onerror = () => resolve(undefined);
}),
key,
) as Promise<PersistedLayout | undefined>;
}
export type Box = { w: number; h: number; x: number; y: number };
/** The card is the player root; the cell is the grid slot it sits in. */
export type BoxTarget = "card" | "cell";
/** One atomic snapshot, or null while any tile is missing or unlaid out. */
function snapshotBoxes(
page: Page,
cameras: readonly string[],
target: BoxTarget,
): Promise<Record<string, Box> | null> {
return page.evaluate(
({ cams, target }) => {
const boxes: Record<string, Box> = {};
for (const cam of cams) {
const card = document.querySelector(`[data-camera='${cam}']`);
const el = target === "cell" ? card?.closest(".p-1") : card;
if (!el) {
return null;
}
const r = el.getBoundingClientRect();
// a re-rendering tile can briefly report no box at all
if (!r.width || !r.height) {
return null;
}
boxes[cam] = { w: r.width, h: r.height, x: r.x, y: r.y };
}
return boxes;
},
{ cams: cameras as readonly string[], target },
);
}
/**
* Measure the given cameras' tiles together, once they have all rendered.
* Measuring in one evaluate is what keeps the numbers mutually comparable.
*/
export async function cameraBoxes<T extends string>(
page: Page,
cameras: readonly T[],
target: BoxTarget = "cell",
): Promise<Record<T, Box>> {
let boxes: Record<string, Box> | null = null;
await expect
.poll(async () => (boxes = await snapshotBoxes(page, cameras, target)), {
timeout: 10_000,
message: `${target}s never rendered for ${cameras.join(", ")}`,
})
.not.toBeNull();
return boxes as unknown as Record<T, Box>;
}
-5
View File
@@ -45,11 +45,6 @@ export class LivePage extends BasePage {
);
}
/** Edit-layout toggle on the draggable grid (desktop, custom groups). */
get editLayoutButton(): Locator {
return this.page.getByTestId("toggle-edit-layout");
}
/** Open the right-click context menu on a camera card (desktop only). */
async openContextMenuOn(cameraName: string): Promise<Locator> {
await this.cameraCard(cameraName).first().click({ button: "right" });
@@ -1,185 +0,0 @@
/**
* Live grid aspect modes.
*
* Bucketed mode (the default) snaps every camera to a wide, landscape or tall
* tile, and converts layouts saved by pre-masonry versions instead of
* discarding them. Natural mode sizes each tile to its own camera.
*/
import { test, expect } from "../fixtures/frigate-test";
import { LivePage } from "../pages/live.page";
import {
cameraBoxes,
persistedLayoutKey,
readLayout,
seedLayout,
type LayoutItem,
} from "../helpers/grid-layout";
const GROUP = "outdoor";
const GRID_COLS = 96;
test.describe("Live grid aspect modes @critical", () => {
test.skip(
({ frigateApp }) => frigateApp.isMobile,
"Draggable grid is desktop-only",
);
test("an ultra-wide camera gets a 32:9 tile in bucketed mode @mobile", async ({
frigateApp,
}) => {
await frigateApp.installDefaults({
config: {
cameras: { backyard: { detect: { width: 2560, height: 720 } } },
},
});
await frigateApp.goto(`/?group=${GROUP}`);
const live = new LivePage(frigateApp.page, true);
await expect(live.cameraCard("backyard").first()).toBeVisible({
timeout: 10_000,
});
const { backyard: wide, front_door: normal } = await cameraBoxes(
frigateApp.page,
["backyard", "front_door"] as const,
);
expect(wide.w / wide.h).toBeCloseTo(32 / 9, 1);
expect(wide.w / normal.w).toBeCloseTo(2, 1);
expect(wide.h).toBeCloseTo(normal.h, 0);
});
test("a letterboxed still image rounds its own corners", async ({
frigateApp,
}) => {
// A portrait camera pillarboxes inside its 8:9 bucket, so the card's
// overflow-hidden clip never reaches the picture's corners. The image has
// to carry the radius itself or it renders with square edges on the tile.
await frigateApp.installDefaults({
config: {
cameras: { backyard: { detect: { width: 720, height: 1280 } } },
},
});
await frigateApp.goto(`/?group=${GROUP}`);
const live = new LivePage(frigateApp.page, true);
await expect(live.cameraCard("backyard").first()).toBeVisible({
timeout: 10_000,
});
const radii = await frigateApp.page.evaluate(() => {
const card = document.querySelector("[data-camera='backyard']");
const img = card?.querySelector("img");
return {
card: card ? getComputedStyle(card).borderTopLeftRadius : null,
img: img ? getComputedStyle(img).borderTopLeftRadius : null,
};
});
expect(radii.card).not.toBe("0px");
expect(radii.img).toBe(radii.card);
});
test("a pre-masonry layout is converted, keeping resized tiles", async ({
frigateApp,
}) => {
await frigateApp.goto(`/?group=${GROUP}`);
const live = new LivePage(frigateApp.page, true);
await expect(live.cameraCard("front_door").first()).toBeVisible({
timeout: 10_000,
});
// 0.17/0.18 shape: bare array on a 12-column grid, 4x4 standard tiles.
// backyard was manually resized to 8x8 and sits beside front_door's column,
// front_door is a standard tile on the row below.
const key = await persistedLayoutKey(frigateApp.page, GROUP);
await seedLayout(frigateApp.page, key, [
{ i: "backyard", x: 4, y: 0, w: 8, h: 8, moved: false, static: false },
{ i: "front_door", x: 0, y: 8, w: 4, h: 4, moved: false, static: false },
]);
await frigateApp.page.reload();
await frigateApp.page.waitForSelector("#pageRoot", { timeout: 10_000 });
await expect(live.cameraCard("front_door").first()).toBeVisible({
timeout: 10_000,
});
// The conversion is written back on first load, replacing the legacy array
// with an envelope. Poll for it: that write is an async idb put.
await expect
.poll(async () => (await readLayout(frigateApp.page, key))?.version, {
timeout: 10_000,
})
.toBe(2);
const stored = (await readLayout(frigateApp.page, key))!;
expect(stored).toMatchObject({ version: 2, naturalAspect: false });
// x and w scale 8x (12 -> 96 columns), y and h scale 18x (4 -> 72 rows per
// standard tile), so the manual resize survives instead of snapping back.
expect(
stored.layout.find((i: LayoutItem) => i.i === "backyard"),
).toMatchObject({
x: 32,
y: 0,
w: 64,
h: 144,
});
expect(
stored.layout.find((i: LayoutItem) => i.i === "front_door"),
).toMatchObject({
x: 0,
y: 144,
w: 32,
h: 72,
});
// arrangement on screen: backyard indented, front_door below it
const { backyard, front_door: frontDoor } = await cameraBoxes(
frigateApp.page,
["backyard", "front_door"] as const,
);
expect(backyard.x).toBeGreaterThan(frontDoor.x + frontDoor.w / 2);
expect(frontDoor.y).toBeGreaterThan(backyard.y + backyard.h / 2);
});
test("conversion is a pure scale, so odd sizes and positions survive", async ({
frigateApp,
}) => {
await frigateApp.goto(`/?group=${GROUP}`);
const live = new LivePage(frigateApp.page, true);
await expect(live.cameraCard("front_door").first()).toBeVisible({
timeout: 10_000,
});
// The old grid exposed all four resize corners with no aspect constraint,
// so a stored tile can be any size. These two are adjacent and non-standard.
const key = await persistedLayoutKey(frigateApp.page, GROUP);
await seedLayout(frigateApp.page, key, [
{ i: "front_door", x: 0, y: 3, w: 5, h: 5 },
{ i: "backyard", x: 5, y: 3, w: 7, h: 5 },
]);
await frigateApp.page.reload();
await frigateApp.page.waitForSelector("#pageRoot", { timeout: 10_000 });
await expect(live.cameraCard("backyard").first()).toBeVisible({
timeout: 10_000,
});
await expect
.poll(async () => (await readLayout(frigateApp.page, key))?.version, {
timeout: 10_000,
})
.toBe(2);
const stored = (await readLayout(frigateApp.page, key))!;
const frontDoor = stored.layout.find(
(i: LayoutItem) => i.i === "front_door",
)!;
const backyard = stored.layout.find((i: LayoutItem) => i.i === "backyard")!;
expect(frontDoor).toMatchObject({ x: 0, y: 54, w: 40, h: 90 });
expect(backyard).toMatchObject({ x: 40, y: 54, w: 56, h: 90 });
// still adjacent, still inside the grid, still not overlapping
expect(frontDoor.x + frontDoor.w).toBe(backyard.x);
expect(backyard.x + backyard.w).toBe(GRID_COLS);
});
});
-298
View File
@@ -1,298 +0,0 @@
/**
* Masonry live grid -- custom-group draggable layout.
*
* Verifies natural-aspect tile sizing and that a saved layout the current
* version cannot read is regenerated cleanly. The grid renders only for a
* custom camera group (here: "outdoor") on desktop; mobile keeps the static
* grid, which the @mobile block below guards.
*/
import { test, expect } from "../fixtures/frigate-test";
import { LivePage } from "../pages/live.page";
import {
cameraBoxes,
persistedLayoutKey,
readLayout,
seedLayout,
} from "../helpers/grid-layout";
const GROUP = "outdoor"; // custom group: front_door + backyard
test.describe("Masonry live grid @critical", () => {
test.skip(
({ frigateApp }) => frigateApp.isMobile,
"Draggable masonry grid is desktop-only",
);
test("custom group renders its cameras in the draggable grid", async ({
frigateApp,
}) => {
await frigateApp.goto(`/?group=${GROUP}`);
const live = new LivePage(frigateApp.page, true);
await expect(live.cameraCard("front_door").first()).toBeVisible({
timeout: 10_000,
});
await expect(live.cameraCard("backyard").first()).toBeVisible();
});
test("tiles render at their camera's natural aspect ratio", async ({
frigateApp,
}) => {
// backyard is 9:16, which bucketed mode would snap to an 8:9 tile
await frigateApp.installDefaults({
config: {
cameras: { backyard: { detect: { width: 720, height: 1280 } } },
},
});
await frigateApp.goto(`/?group=${GROUP}`);
const live = new LivePage(frigateApp.page, true);
await expect(live.cameraCard("front_door").first()).toBeVisible({
timeout: 10_000,
});
await seedLayout(frigateApp.page, "naturalAspectLayout:admin", true);
await frigateApp.page.reload();
await expect(live.cameraCard("backyard").first()).toBeVisible({
timeout: 10_000,
});
const { front_door: landscape, backyard: portrait } = await cameraBoxes(
frigateApp.page,
["front_door", "backyard"] as const,
"card",
);
expect(landscape.w / landscape.h).toBeCloseTo(16 / 9, 1);
expect(portrait.w / portrait.h).toBeCloseTo(9 / 16, 1);
});
test("dragging a tile does not shove other tiles far away", async ({
frigateApp,
}) => {
await frigateApp.goto(`/?group=${GROUP}`);
const live = new LivePage(frigateApp.page, true);
await expect(live.cameraCard("front_door").first()).toBeVisible({
timeout: 10_000,
});
await live.editLayoutButton.click();
const cameras = ["front_door", "backyard"] as const;
const { front_door: fixedBefore, backyard: draggedBox } = await cameraBoxes(
frigateApp.page,
cameras,
"card",
);
// Drag backyard onto front_door's position (a deliberate collision). With
// free-placement + prevent-collision, front_door must NOT be shoved down.
const from = {
x: draggedBox.x + draggedBox.w / 2,
y: draggedBox.y + draggedBox.h / 2,
};
const to = {
x: fixedBefore.x + fixedBefore.w / 2,
y: fixedBefore.y + fixedBefore.h / 2,
};
await frigateApp.page.mouse.move(from.x, from.y);
await frigateApp.page.mouse.down();
await frigateApp.page.mouse.move(to.x, to.y, { steps: 15 });
await frigateApp.page.mouse.up();
const { front_door: fixedAfter } = await cameraBoxes(
frigateApp.page,
cameras,
"card",
);
// Allow a few px of snap; a collision-push would move it a whole tile down.
expect(Math.abs(fixedAfter.y - fixedBefore.y)).toBeLessThan(40);
});
test("resizing a top-row tile preserves its aspect ratio (no pillarboxing)", async ({
frigateApp,
}) => {
// A lone top tile has room to grow sideways, which is what exposed the bug:
// a top-edge handle let width grow while height stayed clamped at y=0.
await frigateApp.installDefaults({
config: { camera_groups: { outdoor: { cameras: ["front_door"] } } },
});
await frigateApp.goto(`/?group=${GROUP}`);
const live = new LivePage(frigateApp.page, true);
await expect(live.cameraCard("front_door").first()).toBeVisible({
timeout: 10_000,
});
await live.editLayoutButton.click();
const tile = frigateApp.page.locator(".react-grid-item", {
has: frigateApp.page.locator("[data-camera='front_door']"),
});
const only = ["front_door"] as const;
const { front_door: before } = await cameraBoxes(
frigateApp.page,
only,
"card",
);
const aspect = before.w / before.h;
// Regression: if a top-edge handle is exposed, dragging it up/out must NOT
// distort the aspect (the old bug grew width while height stayed clamped).
const ne = tile.locator(".react-resizable-handle-ne");
if (await ne.count()) {
await ne.dragTo(tile, {
force: true,
targetPosition: { x: 1000, y: -160 },
});
const { front_door: afterNe } = await cameraBoxes(
frigateApp.page,
only,
"card",
);
// It must actually resize (not a silent no-op) AND keep its aspect.
expect(afterNe.w).toBeGreaterThan(before.w);
expect(Math.abs(afterNe.w / afterNe.h - aspect)).toBeLessThan(0.2);
}
// Positive: growing from the bottom-right corner resizes and keeps aspect.
const se = tile.locator(".react-resizable-handle-se");
await se.dragTo(tile, { force: true, targetPosition: { x: 1000, y: 520 } });
const { front_door: grown } = await cameraBoxes(
frigateApp.page,
only,
"card",
);
expect(grown.w).toBeGreaterThan(before.w);
expect(Math.abs(grown.w / grown.h - aspect)).toBeLessThan(0.2);
});
test("the grid keeps its measured width after a back navigation", async ({
frigateApp,
}) => {
// The grid sizes itself from window.innerWidth until its container is
// measured. On a warm back navigation nothing re-renders after that
// container mounts, so an observer that never attaches leaves every tile
// sized against the full window: the layout widens by the sidebar's width
// and the rightmost column clips on a full row.
await frigateApp.goto(`/?group=${GROUP}`);
const live = new LivePage(frigateApp.page, true);
await expect(live.cameraCard("front_door").first()).toBeVisible({
timeout: 10_000,
});
// total width the tiles span; tracks the width the grid laid out against
const span = () =>
frigateApp.page.evaluate(() => {
const tiles = [...document.querySelectorAll(".react-grid-item")];
if (!tiles.length) {
return null;
}
const rects = tiles.map((tile) => tile.getBoundingClientRect());
return +(
Math.max(...rects.map((r) => r.right)) -
Math.min(...rects.map((r) => r.left))
).toFixed(1);
});
let fresh: number | null = null;
await expect
.poll(async () => (fresh = await span()), { timeout: 10_000 })
.not.toBeNull();
await live.cameraCard("front_door").first().click();
await expect(frigateApp.page).toHaveURL(/#front_door/);
await frigateApp.page.goBack();
await expect(live.cameraCard("front_door").first()).toBeVisible({
timeout: 10_000,
});
// the layout must settle back to the measured width, not window.innerWidth
await expect
.poll(span, { timeout: 10_000 })
.toBeLessThanOrEqual(fresh! + 2);
});
test("a camera added to a saved layout fills an open column", async ({
frigateApp,
}) => {
await frigateApp.goto(`/?group=${GROUP}`);
const live = new LivePage(frigateApp.page, true);
await expect(live.cameraCard("front_door").first()).toBeVisible({
timeout: 10_000,
});
// one tall tile in the first column; backyard is missing from the layout
const key = await persistedLayoutKey(frigateApp.page, GROUP);
await seedLayout(frigateApp.page, key, {
version: 2,
naturalAspect: false,
layout: [{ i: "front_door", x: 0, y: 0, w: 32, h: 400 }],
});
await frigateApp.page.reload();
await expect
.poll(async () => {
const stored = await readLayout(frigateApp.page, key);
return stored?.layout.find((item) => item.i === "backyard");
})
.toMatchObject({ x: 32, y: 0 });
});
test("saved layout from an unreadable version regenerates without error", async ({
frigateApp,
}) => {
await frigateApp.goto(`/?group=${GROUP}`);
const live = new LivePage(frigateApp.page, true);
await expect(live.cameraCard("front_door").first()).toBeVisible({
timeout: 10_000,
});
// A bare array is converted rather than discarded (covered in
// live-grid-aspect-modes), so use a version the current grid cannot read.
const key = await persistedLayoutKey(frigateApp.page, GROUP);
await seedLayout(frigateApp.page, key, {
version: 1,
naturalAspect: false,
layout: [{ i: "front_door", x: 0, y: 0, w: 4, h: 3 }],
});
await frigateApp.page.reload();
await frigateApp.page.waitForSelector("#pageRoot", { timeout: 10_000 });
// Grid regenerated; both cameras still render and the error collector
// (frigate-test fixture) catches any crash.
await expect(live.cameraCard("front_door").first()).toBeVisible({
timeout: 10_000,
});
await expect(live.cameraCard("backyard").first()).toBeVisible();
// The app must have replaced the value it could not read. Without this the
// test would still pass against a key the app never touches.
await expect
.poll(async () => (await readLayout(frigateApp.page, key))?.version, {
timeout: 10_000,
})
.toBeGreaterThan(1);
});
});
test.describe("Masonry live grid on mobile @critical @mobile", () => {
test("custom group keeps the static grid, with no draggable layout", async ({
frigateApp,
}) => {
test.skip(!frigateApp.isMobile, "Mobile-only");
await frigateApp.goto(`/?group=${GROUP}`);
const live = new LivePage(frigateApp.page, false);
await expect(live.cameraCard("front_door").first()).toBeVisible({
timeout: 10_000,
});
await expect(live.cameraCard("backyard").first()).toBeVisible();
// isMobileOnly routes around DraggableGridLayout entirely, so neither the
// grid items nor the edit-layout toggle may appear.
await expect(frigateApp.page.locator(".react-grid-item")).toHaveCount(0);
await expect(live.editLayoutButton).toHaveCount(0);
});
});
@@ -217,23 +217,6 @@ test.describe("Detection models settings @high", () => {
).toBeDisabled();
});
test("shareable hardware another model uses can still be picked", async ({
frigateApp,
}) => {
await installRoutes(frigateApp.page, [
{ scene: "all", devices: ["openvino:GPU.0"] },
{ scene: "outdoor", devices: ["openvino:GPU.1"] },
]);
await openPage(frigateApp);
await expect(
frigateApp.page.locator("#models-0-openvino\\:GPU\\.1").first(),
).toBeEnabled();
await expect(frigateApp.page.locator("#pageRoot")).not.toContainText(
"used by outdoor",
);
});
test("adding a model appends a card with an unused scene", async ({
frigateApp,
}) => {
@@ -265,13 +248,15 @@ test.describe("Detection models settings @high", () => {
test("a saved Frigate+ model opens on the Frigate+ tab", async ({
frigateApp,
}) => {
// the backend resolves plus:// to a cache path before serving the config
// back, so the plus metadata is the only signal the model is a Plus one
await installRoutes(
frigateApp.page,
[
{
scene: "all",
devices: ["openvino:GPU.0"],
path: "plus://abc123",
path: "/config/model_cache/abc123",
plus: PLUS_MODEL,
},
],
@@ -317,65 +302,6 @@ test.describe("Detection models settings @high", () => {
expect(saves.at(-1)?.config_data?.models?.[0].path).toBe("plus://abc123");
});
test("saving a Frigate+ model keeps its reference without the Frigate+ fields", async ({
frigateApp,
}) => {
// the backend fills these in from the Frigate+ model info when it loads
const saves = await installRoutes(
frigateApp.page,
[
{
scene: "all",
devices: ["openvino:GPU.0"],
path: "plus://abc123",
plus: PLUS_MODEL,
width: 320,
height: 320,
input_tensor: "nchw",
input_dtype: "float",
model_type: "yolo-generic",
},
],
true,
);
await openPage(frigateApp);
await frigateApp.page.locator("#models-0-openvino\\:GPU\\.1").click();
await frigateApp.page.getByRole("button", { name: /^Save$/ }).click();
await expect.poll(() => saves.length).toBeGreaterThan(0);
const model = saves.at(-1)?.config_data?.models?.[0];
expect(model?.path).toBe("plus://abc123");
expect(model?.devices).toEqual(["openvino:GPU.0", "openvino:GPU.1"]);
expect(model).not.toHaveProperty("width");
expect(model).not.toHaveProperty("input_tensor");
expect(model).not.toHaveProperty("model_type");
// a leftover dtype from a custom model must not override the int default
expect(model).not.toHaveProperty("input_dtype");
});
test("a Frigate+ model only shows its path without a Frigate+ API key", async ({
frigateApp,
}) => {
await installRoutes(frigateApp.page, [
{
scene: "all",
devices: ["openvino:GPU.0"],
path: "plus://abc123",
width: 320,
height: 320,
},
]);
await openPage(frigateApp);
const root = frigateApp.page.locator("#pageRoot");
await expect(root).toContainText("Custom object detector model path");
await expect(root).not.toContainText("Object detection model input width");
await expect(root).not.toContainText(
"Label map for custom object detector",
);
});
test("a Frigate+ Hailo model is listed by the device it was built for", async ({
frigateApp,
}) => {
@@ -13,26 +13,7 @@ import type { Page } from "@playwright/test";
const OUTDOOR_LAYOUT_KEY = "outdoor-draggable-layout:admin";
const STREAMING_KEY = "streaming-settings:admin";
// the shape DraggableGridLayout writes
const OUTDOOR_LAYOUT = {
version: 2,
naturalAspect: false,
layout: [
{ i: "front_door", x: 0, y: 0, w: 32, h: 72 },
{ i: "backyard", x: 32, y: 0, w: 32, h: 72 },
],
};
const NATURAL_OUTDOOR_LAYOUT = {
version: 2,
naturalAspect: true,
layout: [
{ i: "front_door", x: 0, y: 0, w: 32, h: 72 },
{ i: "backyard", x: 32, y: 0, w: 24, h: 96 },
],
};
const LEGACY_OUTDOOR_LAYOUT = [
const OUTDOOR_LAYOUT = [
{ i: "front_door", x: 0, y: 0, w: 6, h: 4 },
{ i: "backyard", x: 6, y: 0, w: 6, h: 4 },
];
@@ -178,160 +159,6 @@ test.describe("UI settings import/export @medium", () => {
expect(payload.sections.preferences.playbackRate).toBe(2);
});
test("exports only layouts built for the current tile sizing mode", async ({
frigateApp,
}) => {
// a group not opened since the mode changed still holds a layout from
// the other mode, which would import into a mode that cannot show it
await frigateApp.goto("/settings?page=uiSettings");
await writeIdb(frigateApp.page, {
[OUTDOOR_LAYOUT_KEY]: OUTDOOR_LAYOUT,
"default-draggable-layout:admin": NATURAL_OUTDOOR_LAYOUT,
"naturalAspectLayout:admin": true,
});
const downloadPromise = frigateApp.page.waitForEvent("download");
await frigateApp.page
.getByRole("button", { name: "Export Settings" })
.click();
const download = await downloadPromise;
const payload = JSON.parse(readFileSync((await download.path())!, "utf-8"));
expect(payload.sections.layouts).toEqual({
default: NATURAL_OUTDOOR_LAYOUT,
});
});
test("toggling tile sizing mode clears stored layouts", async ({
frigateApp,
}) => {
test.skip(frigateApp.isMobile, "The setting is hidden on phones");
await frigateApp.goto("/settings?page=uiSettings");
await writeIdb(frigateApp.page, { [OUTDOOR_LAYOUT_KEY]: OUTDOOR_LAYOUT });
await frigateApp.page.locator("#natural-aspect-desktop").click();
await frigateApp.page
.getByRole("alertdialog")
.getByRole("button", { name: "Enable" })
.click();
await expect
.poll(() => readIdb(frigateApp.page, OUTDOOR_LAYOUT_KEY))
.toBeNull();
expect(await readIdb(frigateApp.page, "naturalAspectLayout:admin")).toBe(
true,
);
});
test("round-trips a layout left unconverted by an upgrade", async ({
frigateApp,
}) => {
test.skip(frigateApp.isMobile, "Layout import is desktop and tablet only");
// DraggableGridLayout rewrites a pre-0.19 layout only when that group's
// dashboard is opened, so exporting first carries the bare array into the
// file. Import must accept it back rather than rejecting the whole file.
await frigateApp.goto("/settings?page=uiSettings");
await writeIdb(frigateApp.page, {
[OUTDOOR_LAYOUT_KEY]: LEGACY_OUTDOOR_LAYOUT,
"playbackRate:admin": 2,
});
const downloadPromise = frigateApp.page.waitForEvent("download");
await frigateApp.page
.getByRole("button", { name: "Export Settings" })
.click();
const download = await downloadPromise;
const contents = readFileSync((await download.path())!, "utf-8");
expect(JSON.parse(contents).sections.layouts.outdoor).toEqual(
LEGACY_OUTDOOR_LAYOUT,
);
await clearIdb(frigateApp.page);
await chooseImportText(frigateApp.page, contents);
await confirmImport(frigateApp.page);
expect(await readIdb(frigateApp.page, OUTDOOR_LAYOUT_KEY)).toEqual(
LEGACY_OUTDOOR_LAYOUT,
);
// the rest of the file must survive alongside it
expect(await readIdb(frigateApp.page, "playbackRate:admin")).toBe(2);
});
test("legacy layouts import turns natural aspect off so they display", async ({
frigateApp,
}) => {
test.skip(frigateApp.isMobile, "Layout import is desktop and tablet only");
// Bare-array layouts only render in bucketed mode; with natural aspect on
// they would be discarded and regenerated on the next dashboard visit. The
// import applies the mode the layouts were built for, and the file's own
// naturalAspectLayout preference must not override that.
await frigateApp.goto("/settings?page=uiSettings");
await writeIdb(frigateApp.page, { "naturalAspectLayout:admin": true });
await chooseImportFile(
frigateApp.page,
importPayload({
sections: {
layouts: { outdoor: LEGACY_OUTDOOR_LAYOUT },
streaming: {},
preferences: { naturalAspectLayout: true },
},
}),
);
const note = frigateApp.page.getByText(/standard tile sizing/);
await expect(note).toBeVisible();
// the note is about the layouts section, so it follows its switch
await frigateApp.page.getByText("Camera group layouts (1 group)").click();
await expect(note).toBeHidden();
await frigateApp.page.getByText("Camera group layouts (1 group)").click();
await expect(note).toBeVisible();
await confirmImport(frigateApp.page);
expect(await readIdb(frigateApp.page, OUTDOOR_LAYOUT_KEY)).toEqual(
LEGACY_OUTDOOR_LAYOUT,
);
expect(await readIdb(frigateApp.page, "naturalAspectLayout:admin")).toBe(
false,
);
});
test("natural aspect layouts import turns the setting on", async ({
frigateApp,
}) => {
test.skip(frigateApp.isMobile, "Layout import is desktop and tablet only");
await frigateApp.goto("/settings?page=uiSettings");
await chooseImportFile(
frigateApp.page,
importPayload({
sections: {
layouts: { outdoor: NATURAL_OUTDOOR_LAYOUT },
streaming: {},
preferences: {},
},
}),
);
await expect(
frigateApp.page.getByText(/camera aspect ratio tile sizing/),
).toBeVisible();
await confirmImport(frigateApp.page);
expect(await readIdb(frigateApp.page, OUTDOOR_LAYOUT_KEY)).toEqual(
NATURAL_OUTDOOR_LAYOUT,
);
expect(await readIdb(frigateApp.page, "naturalAspectLayout:admin")).toBe(
true,
);
});
test("omits settings that were never stored", async ({ frigateApp }) => {
await frigateApp.goto("/settings?page=uiSettings");
@@ -363,15 +190,12 @@ test.describe("UI settings import/export @medium", () => {
await expect(
frigateApp.page.getByText("UI preferences (2 settings)"),
).toBeVisible();
// patio is layout-only, so its warning follows the layouts section
await expect(frigateApp.page.getByText(/patio/)).toBeVisible({
visible: !frigateApp.isMobile,
});
await expect(frigateApp.page.getByText(/patio/)).toBeVisible();
await confirmImport(frigateApp.page);
expect(await readIdb(frigateApp.page, OUTDOOR_LAYOUT_KEY)).toEqual(
frigateApp.isMobile ? null : OUTDOOR_LAYOUT,
OUTDOOR_LAYOUT,
);
expect(await readIdb(frigateApp.page, STREAMING_KEY)).toEqual(
STREAMING_SETTINGS,
@@ -382,7 +206,6 @@ test.describe("UI settings import/export @medium", () => {
test("hides the unknown-group warning when layouts are switched off", async ({
frigateApp,
}) => {
test.skip(frigateApp.isMobile, "Layout import is desktop and tablet only");
await frigateApp.goto("/settings?page=uiSettings");
// patio is a layout-only group absent from this server, so the warning
@@ -412,39 +235,7 @@ test.describe("UI settings import/export @medium", () => {
expect(await readIdb(frigateApp.page, STREAMING_KEY)).toEqual({});
expect(await readIdb(frigateApp.page, OUTDOOR_LAYOUT_KEY)).toEqual(
frigateApp.isMobile ? null : OUTDOOR_LAYOUT,
);
});
test("phones refuse layouts and say why @mobile", async ({ frigateApp }) => {
test.skip(!frigateApp.isMobile, "Phone-only");
await frigateApp.goto("/settings?page=uiSettings");
await writeIdb(frigateApp.page, { "naturalAspectLayout:admin": false });
await chooseImportFile(frigateApp.page, importPayload());
await expect(
frigateApp.page.getByText(/aren't imported on phones/),
).toBeVisible();
// the section is still listed, but cannot be switched on
await expect(
frigateApp.page.getByText("Camera group layouts (2 groups)"),
).toBeVisible();
await expect(
frigateApp.page.locator('[id="Camera group layouts (2 groups)"]'),
).toBeDisabled();
await confirmImport(frigateApp.page);
expect(await readIdb(frigateApp.page, OUTDOOR_LAYOUT_KEY)).toBeNull();
expect(await readIdb(frigateApp.page, STREAMING_KEY)).toEqual(
STREAMING_SETTINGS,
);
// a layouts import is what flips this, so it must stay put
expect(await readIdb(frigateApp.page, "naturalAspectLayout:admin")).toBe(
false,
OUTDOOR_LAYOUT,
);
});
@@ -534,7 +325,7 @@ test.describe("UI settings import/export @medium", () => {
await confirmImport(frigateApp.page);
expect(await readIdb(frigateApp.page, OUTDOOR_LAYOUT_KEY)).toEqual(
frigateApp.isMobile ? null : OUTDOOR_LAYOUT,
OUTDOOR_LAYOUT,
);
expect(await readIdb(frigateApp.page, STREAMING_KEY)).toEqual(
STREAMING_SETTINGS,
@@ -1223,6 +1223,7 @@
"desc": "Frigate можа сам адпраўляць push-апавяшчэнні на вашу прыладу, калі працуе ў браўзеры або ўсталяваны як PWA."
},
"notificationUnavailable": {
"title": "Апавяшчэнні недаступныя",
"desc": "Вэб-push-апавяшчэнні патрабуюць бяспечнага кантэксту (<code>https://…</code>). Гэта абмежаванне браўзера. Каб карыстацца апавяшчэннямі, адкрывайце Frigate па бяспечным пратаколе.",
"descPwa": "У iOS вэб-push-апавяшчэнні даступныя, толькі калі Frigate дададзены на хатні экран. Адкрыйце меню <strong>Абагуліць</strong>, выберыце <strong>На хатні экран</strong>, а потым адкрыйце Frigate праз новы значок, каб зарэгістраваць гэту прыладу для апавяшчэнняў."
},
@@ -1330,6 +1331,7 @@
"triggers": {
"documentTitle": "Трыгеры",
"semanticSearch": {
"title": "Семантычны пошук адключаны",
"desc": "Каб карыстацца трыгерамі, трэба ўключыць семантычны пошук."
},
"management": {
+10
View File
@@ -236,6 +236,8 @@
},
"lastRefreshed": "Апошняе абнаўленне: ",
"stats": {
"ffmpegHighCpuUsage": "{{camera}} моцна нагружае CPU праз FFmpeg ({{ffmpegAvg}}%)",
"detectHighCpuUsage": "{{camera}} моцна нагружае CPU дэтэктаваннем ({{detectAvg}}%)",
"healthy": "Сістэма спраўная",
"reindexingEmbeddings": "Пераіндэксацыя ўбудаванняў (гатова {{processed}}%)",
"cameraIsOffline": "{{camera}} па-за сеткай",
@@ -279,12 +281,20 @@
"notices": {
"title": "Заўвагі",
"empty": "Ваша ўсталёўка Frigate у парадку",
"dismiss": "Схаваць",
"openSettings": "Адкрыць налады",
"openLink": "Адкрыць спасылку",
"noMatches": "Няма заўваг, якія адпавядаюць фільтру",
"dismissedTitle": "Схаваныя",
"noneDismissed": "Няма схаваных заўваг",
"clearDismissed": "Ачысціць схаваныя",
"clearDismissedTitle": "Ачысціць схаваныя заўвагі?",
"clearDismissedDesc": "Усе схаваныя заўвагі будуць выдалены. Праверкі канфігурацыі і плыняў пакажуцца адразу, а іншыя заўвагі вернуцца пры наступным узнікненні.",
"firstSeen_one": "Упершыню {{time}}",
"firstSeen_other": "Упершыню {{time}} · {{count}} разу",
"dismissedAt": "Схавана {{time}}",
"filter": {
"showDismissed": "Паказаць схаваныя",
"severity": "Узровень важнасці",
"error": "Памылка",
"warning": "Папярэджанне",
@@ -1037,6 +1037,7 @@
"desc": "Frigate može nativno slati obavijesti na vaš uređaj kada radi u pregledaču ili je instalirana kao PWA."
},
"notificationUnavailable": {
"title": "Obavijesti nedostupne",
"desc": "Web obavijesti zahtijevaju sigurni kontekst (<code>https://…</code>). Ovo je ograničenje pregledača. Pristupite Frigate sigurno da biste koristili obavijesti."
},
"globalSettings": {
@@ -1134,6 +1135,7 @@
"triggers": {
"documentTitle": "Pokretači",
"semanticSearch": {
"title": "Semantička pretraga je onemogućena",
"desc": "Semantička pretraga mora biti omogućena da biste koristili izazivače."
},
"management": {
+2
View File
@@ -211,6 +211,8 @@
},
"lastRefreshed": "Posljednje ažuriranje: ",
"stats": {
"ffmpegHighCpuUsage": "{{camera}} ima visoku upotrebu CPU za FFmpeg ({{ffmpegAvg}}%)",
"detectHighCpuUsage": "{{camera}} ima visoku upotrebu CPU za detekciju ({{detectAvg}}%)",
"healthy": "Sistem je zdrav",
"reindexingEmbeddings": "Ponovno indeksiranje ugrađenih vjerodajnica ({{processed}}% završeno)",
"cameraIsOffline": "{{camera}} je offline",
+6 -9
View File
@@ -119,9 +119,6 @@
"liveFallbackTimeout": {
"label": "Temps d'espera per a la reserva del jugador en directe",
"desc": "Quan el flux en viu d'alta qualitat d'una càmera no està disponible, torneu al mode d'amplada de banda baixa després d'aquests molts segons. Per defecte: 3."
},
"naturalAspectLayout": {
"label": "Utilitza relació natural d'aspecte"
}
},
"storedLayouts": {
@@ -458,8 +455,9 @@
},
"title": "Notificacions",
"notificationUnavailable": {
"desc": "Les notificacions push web requereixen un context segur (https://…). Aquesta és una limitació del navegador. Accedeix a Frigate de manera segura per utilitzar les notificacions.",
"descPwa": "A iOS, les notificacions push web només estàn disponibles quan Frigate està instalat a la pantalla principal. Obre el menú Compartir, selecciona Afegir a la pantalla, i obre Frigate des del nou icona per registrar les notificacions en aquest dispositiu."
"title": "Notificacions no disponibles",
"desc": "Les notificacions push web requereixen un context segur (<code>https://…</code>). Aquesta és una limitació del navegador. Accedeix a Frigate de manera segura per utilitzar les notificacions.",
"descPwa": "A iOS, les notificacions push web només estàn disponibles quan Frigate està instalat a la pantalla principal. Obre el menú <strong>Compartir</strong> , selecciona <strong>Afegir a la pantalla</strong>, i obre Frigate des del nou icona per registrar les notificacions en aquest dispositiu."
},
"unsavedChanges": "Canvis de notificació no desats",
"globalSettings": {
@@ -883,7 +881,8 @@
},
"addTrigger": "Afegir disaprador",
"semanticSearch": {
"desc": "La cerca semàntica ha d'estar activada per a utilitzar els activadors."
"desc": "La cerca semàntica ha d'estar activada per a utilitzar els activadors.",
"title": "La cerca semàntica està desactivada"
},
"wizard": {
"title": "Crea un activador",
@@ -1545,9 +1544,7 @@
"preset-record-generic-audio-aac": "Enregistra (Genèric + Àudio a AAC)",
"preset-record-mjpeg": "Registre - Càmeres MJPEG",
"preset-record-jpeg": "Registre - Càmeres JPEG",
"preset-record-ubiquiti": "Registre - Càmeres Ubiquiti",
"preset-apple-silicon-h265": "Apple Silicon (H.265)",
"preset-apple-silicon-h264": "Apple Silicon (H.264)"
"preset-record-ubiquiti": "Registre - Càmeres Ubiquiti"
},
"sameAsRecord": "Igual que els arguments de sortida del registre"
},
+14 -22
View File
@@ -239,15 +239,15 @@
"reindexingEmbeddings": "Reindexant vectors ({{processed}}% completat)",
"healthy": "El sistema és saludable",
"cameraIsOffline": "{{camera}} està fora de línia",
"ffmpegHighCpuUsage": "{{camera}} te un ús elevat de CPU per FFmpeg ({{ffmpegAvg}}%)",
"detectHighCpuUsage": "{{camera}} te un ús elevat de CPU per la detecció ({{detectAvg}}%)",
"detectIsVerySlow": "{{detect}} és molt lent ({{speed}} ms)",
"detectIsSlow": "{{detect}} és lent ({{speed}} ms)",
"debugReplayActive": "La sessió de repetició de depuració està activa",
"cameraSkippedDetections": "{{camera}} està ometent la detecció en {{pct}}% de fotogrames",
"systemNotices_one": "{{count}} avís del sistema",
"systemNotices_other": "{{count}} avisos del sistema",
"retentionUnmet": "Els enregistraments s'estan suprimint abans que acabi el seu període de retenció a l'espai lliure",
"moreMessages_other": "+{{count}}",
"moreMessages_one": "+{{count}}"
"retentionUnmet": "Els enregistraments s'estan suprimint abans que acabi el seu període de retenció a l'espai lliure"
},
"enrichments": {
"title": "Anàlisi avançada",
@@ -281,17 +281,24 @@
"notices": {
"title": "Avís",
"empty": "La instal·lació de Frigate és saludable",
"dismiss": "Descarta",
"openSettings": "Obre la configuració",
"openLink": "Obre l'enllaç",
"noMatches": "No hi ha avisos que coincideixin amb el filtre",
"dismissedTitle": "Descartat",
"noneDismissed": "No hi ha avisos descartats",
"clearDismissed": "Neteja descartada",
"clearDismissedTitle": "Voleu netejar els avisos descartats?",
"clearDismissedDesc": "S'elimina cada avís acomiadat. Les comprovacions de configuració i flux es mostren de nou immediatament, i altres avisos tornen la propera vegada que succeeixin.",
"firstSeen_one": "{{time}} vist per primera vegada",
"firstSeen_other": "{{time}} · {{count}} vegades",
"dismissedAt": "S'ha suprimit {{time}}",
"filter": {
"showDismissed": "Mostra descartat",
"severity": "Severitat",
"warning": "Avís",
"error": "Error",
"info": "Info",
"showHidden": "Mostra els ocults"
"info": "Info"
},
"kinds": {
"detector_stuck": "S'ha reiniciat el detector {{detector}} després que deixés de respondre",
@@ -300,27 +307,12 @@
"shm_too_low": "L'assignació /dev/shm ({{total}} MB) s'hauria d'augmentar a com a mínim {{min}} MB",
"failed_login_one": "Error en l'intent d'inici de sessió de {{user}}",
"failed_login_other": "Intents d'inici de sessió fallits per {{user}}",
"update_available": "Frigate {{version}} està disponible",
"ffmpeg_high_cpu": "L'ús de la CPU FFmpeg és alt ({{cpu}}% mitjana)",
"detect_high_cpu": "L'ús de la CPU de detecció és alt ({{cpu}}% mitjana)"
"update_available": "Frigate {{version}} està disponible"
},
"streamPrefix": "Flux {{index}}: {{message}}",
"streamPrefixRestream": "Flux {{index}} (via go2rtc): {{message}}",
"streamProbeFailed": "No s'ha pogut provar el flux {{index}}: {{error}}",
"cameraProbeFailed": "No s'han pogut explorar els fluxos: {{error}}",
"acknowledge": "Agraïment",
"acknowledgeHint": "Amaga fins que això torni a passar",
"mute": "Silenciar",
"muteHint": "No ho mostris mai més",
"unmute": "No silenciis",
"showAgain": "Torna a mostrar",
"hiddenTitle": "Ocultar",
"noneHidden": "No hi ha avisos ocults",
"showAll": "Mostra-ho tot de nou",
"showAllTitle": "Voleu mostrar tots els avisos ocults?",
"showAllDesc": "Cada avís reconegut i silenciat torna a la llista d'avisos, incloent-hi les comprovacions de configuració i flux.",
"acknowledgedAt": "Reconegut {{time}}",
"mutedAt": "Mutat {{time}}"
"cameraProbeFailed": "No s'han pogut explorar els fluxos: {{error}}"
},
"hardware": {
"title": "Maquinari",
@@ -374,6 +374,7 @@
"desc": "Frigate může nativně odesílat push notifikace do vašeho zařízení, pokud běží v prohlížeči nebo je nainstalován jako PWA (progresivní webová aplikace)."
},
"notificationUnavailable": {
"title": "Notifikace Nedostupné",
"desc": "Webové push notifikace vyžadují zabezpečený kontext (<code>https://…</code>). Jedná se o omezení prohlížeče. Pro použití notifikací přistupujte k Frigate přes zabezpečené připojení."
},
"cameras": {
+3 -1
View File
@@ -50,7 +50,9 @@
"healthy": "Systém je zdravý",
"reindexingEmbeddings": "Přeindexování vektorů ({{processed}} % dokončeno)",
"detectIsSlow": "{{detect}} je pomalé ({{speed}} ms)",
"detectIsVerySlow": "{{detect}} je velmi pomalé ({{speed}} ms)"
"detectIsVerySlow": "{{detect}} je velmi pomalé ({{speed}} ms)",
"detectHighCpuUsage": "{{camera}} má vysoké využití CPU detekcemi ({{detectAvg}} %)",
"ffmpegHighCpuUsage": "{{camera}} má vyské využití CPU FFmpegem ({{ffmpegAvg}}%)"
},
"enrichments": {
"embeddings": {
+1 -67
View File
@@ -346,71 +346,5 @@
"bluegrass": "Bluegrass",
"folk_music": "Folkemusik",
"echo": "Ekko",
"pulse": "Puls",
"funk": "Funk",
"middle_eastern_music": "Mellemøstlig musik",
"carnatic_music": "Karnatisk musik",
"music_of_bollywood": "Bollywood musik",
"dental_drill's_drill": "Tandlægebor",
"medium_engine": "Mellemstor motor",
"heavy_engine": "Kraftig motor",
"engine_knocking": "Motorbanken",
"engine_starting": "Motor, der starter",
"idling": "Tomgang",
"accelerating": "Acceleration",
"ding-dong": "Ding-dong",
"sliding_door": "Skydedør",
"tap": "let bank",
"cupboard_open_or_close": "Skab åbnes eller lukkes",
"drawer_open_or_close": "Skuffe åbnes eller lukkes",
"chopping": "Hugning",
"frying": "Stegning",
"microwave_oven": "Mikrobølgeovn",
"water_tap": "Vandhane",
"toilet_flush": "Toiletskylning",
"electric_toothbrush": "Elektrisk tandbørste",
"vacuum_cleaner": "Støvsuger",
"keys_jangling": "Nøgleraslen",
"electric_shaver": "Elektrisk barbermaskine",
"shuffling_cards": "Kortblanding",
"typing": "Skrivning (på tastatur)",
"computer_keyboard": "Computertastatur",
"writing": "Skrivning",
"telephone_bell_ringing": "Telefonringen",
"telephone_dialing": "Telefonopkald",
"dial_tone": "Summetone",
"busy_signal": "Optaget-signal",
"alarm_clock": "Vækkeur",
"civil_defense_siren": "Luftsirene",
"buzzer": "Summer",
"smoke_detector": "Røgalarm",
"fire_alarm": "Brandalarm",
"steam_whistle": "Dampfløjte",
"mechanisms": "Mekanismer",
"ratchet": "Skralde",
"tick": "Tik",
"tick-tock": "Tik-tak",
"gears": "Tandhjul",
"pulleys": "Trisser",
"sewing_machine": "Symaskine",
"mechanical_fan": "Mekanisk blæser",
"air_conditioning": "Air Condition",
"cash_register": "Kasseapparat",
"single-lens_reflex_camera": "Spejlreflekskamera",
"jackhammer": "Trykkluftbor",
"sawing": "Savning",
"filing": "Filning",
"sanding": "Slibning",
"power_tool": "Elværktøj",
"gunshot": "Skud",
"machine_gun": "Maskingevær",
"fusillade": "Salve",
"artillery_fire": "Artilleriild",
"cap_gun": "Knaldpistol",
"firecracker": "Kanonslag",
"burst": "Sprang",
"eruption": "Udbrud",
"boom": "Bum",
"wood": "Træ",
"chop": "Hugge"
"pulse": "Puls"
}
@@ -580,6 +580,7 @@
},
"title": "Benachrichtigungen",
"notificationUnavailable": {
"title": "Benachrichtigungen nicht verfügbar",
"desc": "Web Push Benachrichtigungen erfordern einen sicheren Kontext (<code>https://…</code>). Das ist eine Vorgabe des Browsers. Greife auf Frigate gesichert zu um Benachrichtigungen zu nutzen.",
"descPwa": "Unter iOS sind Web-Push-Benachrichtigungen nur verfügbar, wenn Frigate auf Ihrem Startbildschirm installiert ist. Öffnen Sie das Menü <strong>Teilen</strong>, wählen Sie <strong>Zum Startbildschirm hinzufügen</strong> und öffnen Sie Frigate über das neue Symbol, um dieses Gerät für Benachrichtigungen zu registrieren."
},
@@ -838,6 +839,7 @@
}
},
"semanticSearch": {
"title": "Semantische Suche ist deaktiviert",
"desc": "Semantische Suche muss aktiviert sein um Auslöser nutzen zu können."
},
"wizard": {
+2
View File
@@ -254,6 +254,8 @@
},
"stats": {
"healthy": "Das System läuft problemlos",
"ffmpegHighCpuUsage": "{{camera}} hat eine hohe FFmpeg CPU Auslastung ({{ffmpegAvg}}%)",
"detectHighCpuUsage": "{{camera}} hat eine hohe CPU Auslastung bei der Erkennung ({{detectAvg}}%)",
"reindexingEmbeddings": "Neuindizierung von Einbettungen ({{processed}}% erledigt)",
"detectIsSlow": "{{detect}} ist langsam ({{speed}} ms)",
"detectIsVerySlow": "{{detect}} ist sehr langsam ({{speed}} ms)",
-1
View File
@@ -131,7 +131,6 @@
"close": "Close",
"expand": "Expand",
"collapse": "Collapse",
"clear": "Clear",
"copy": "Copy",
"copiedToClipboard": "Copied to clipboard",
"back": "Back",
+3 -13
View File
@@ -167,11 +167,6 @@
"label": "Always Show Camera Names",
"desc": "Always show the camera names in a chip in the multi-camera live view dashboard."
},
"naturalAspectLayout": {
"label": "Use Natural Aspect Ratios",
"desc": "On camera group live view dashboards, size each tile to its camera's own natural aspect ratio. When disabled, cameras are snapped to a standard wide, landscape, or tall tile shape.",
"descNote": "Toggling this setting on or off will clear the stored layout for all camera group live dashboards. Manual reconfiguration will be required."
},
"liveFallbackTimeout": {
"label": "Live Player Fallback Timeout",
"desc": "When a camera's high quality live stream is unavailable, fall back to low bandwidth mode after this many seconds. Default: 3."
@@ -180,14 +175,12 @@
"storedLayouts": {
"title": "Stored Layouts",
"desc": "The layout of cameras in a camera group can be dragged/resized. The positions are stored in your browser's local storage.",
"clearAll": "Clear All Layouts",
"clearConfirm": "This will clear the stored layout for every camera group in this browser. This cannot be undone."
"clearAll": "Clear All Layouts"
},
"cameraGroupStreaming": {
"title": "Camera Group Streaming Settings",
"desc": "Streaming settings for each camera group are stored in your browser's local storage.",
"clearAll": "Clear All Streaming Settings",
"clearConfirm": "This will clear the streaming settings for every camera group in this browser. This cannot be undone."
"clearAll": "Clear All Streaming Settings"
},
"backupRestore": {
"title": "Backup & Restore",
@@ -202,9 +195,6 @@
"desc": "Choose what to apply from this file. Frigate will reload when the import finishes.",
"exportedFrom": "Exported {{date}} from Frigate config version {{version}}",
"layouts_one": "Camera group layouts ({{count}} group)",
"layoutsPhone": "Camera group layouts aren't imported on phones, which always use the standard grid.",
"layoutsModeOn": "These layouts use camera aspect ratio tile sizing, so importing them will also turn on \"Use Natural Aspect Ratios\".",
"layoutsModeOff": "These layouts use standard tile sizing, so importing them will also turn off \"Use Natural Aspect Ratios\".",
"layouts_other": "Camera group layouts ({{count}} groups)",
"streaming_one": "Streaming settings ({{count}} camera)",
"streaming_other": "Streaming settings ({{count}} cameras)",
@@ -2063,7 +2053,7 @@
"unrecognized": "This model is configured for hardware that was not found on this system: {{devices}}",
"description": "The hardware this model runs its detection on.",
"detectorCountDescription": "How many detection processes to run on this hardware. More detectors keep up with more cameras, at the cost of extra device memory.",
"unitsDescription": "Each unit runs its own detection process. A unit that can't be shared, such as a Coral, can only be used by one model."
"unitsDescription": "Each unit runs its own detection process. A unit already used by another model can not be selected."
},
"tabs": {
"plus": "Frigate+",
+1 -16
View File
@@ -137,15 +137,6 @@
"partial": "Se iniciaron {{successful}} de {{total}} exportaciones. Fallidas: {{failedItems}}",
"failed": "No se pudieron iniciar {{total}} exportaciones. Fallidas: {{failedItems}}"
}
},
"stream": {
"label": "Calidad",
"auto": "Auto",
"main": "Original",
"sub": "Bajo",
"autoDesc": "Usa el stream princial y vuelve a la calidad baja del sub-stream si la principal no está disponible.",
"mainDesc": "Exporta sólo la calidad original del stream principal. Los tiempos dónde el stream principal no esté disponible estarán faltantes.",
"subDesc": "Exporta sólo la calidad inferior del sub-stream. Los tiempos dónde el sub-steam no está disponible faltarán."
}
},
"streaming": {
@@ -191,8 +182,7 @@
"export": "Exportar",
"markAsReviewed": "Marcar como revisado",
"deleteNow": "Eliminar ahora",
"markAsUnreviewed": "Marcar como no revisado",
"generateDescription": "Generar descripción"
"markAsUnreviewed": "Marcar como no revisado"
},
"shareTimestamp": {
"description": "Comparta una URL con marca de tiempo de la posición actual del reproductor o elija una marca de tiempo personalizada. Tenga en cuenta que esta no es una URL pública para compartir y solo es accesible para los usuarios que tienen acceso a Frigate y a esta cámara.",
@@ -201,11 +191,6 @@
"custom": "Marca de tiempo personalizada",
"button": "Compartir URL de la marca de tiempo",
"shareTitle": "Marca de tiempo de revisión de Frigate: {{camera}}"
},
"genaiDescription": {
"toast": {
"error": "Falló al pedir descripción: {{error}}"
}
}
},
"imagePicker": {
+2 -17
View File
@@ -35,25 +35,10 @@
"submittedFrigatePlus": "Fotograma enviado correctamente a Frigate+"
},
"error": {
"submitFrigatePlusFailed": "Error al enviar el fotograma a Frigate+",
"playRecordingsFailed": "Falló al reproducir grabaciones(error{{code}}:{{message}})"
"submitFrigatePlusFailed": "Error al enviar el fotograma a Frigate+"
}
},
"livePlayerRequiredIOSVersion": "Se requiere iOS 17.1 o superior para este tipo de transmisión en vivo.",
"noRecordingsFoundForThisTime": "No se encontraron grabaciones para este momento",
"cameraOff": "La cámara está apagada",
"quality": {
"auto": "Auto",
"autoLow": "Reproduciendo con baja calidad (ancho de banda limitado)",
"autoLowCodec": "Reproduciendo con baja calidad (Este navegador no soporta la calidad original)",
"autoLowSaveData": "Reproduciendo con baja calidad (ahorro de datos)",
"main": "Original",
"sub": "Baja",
"notSupportedBrowser": "No soportado por este navegador",
"label": "Calidad",
"noAudio": "Sin audio",
"audioRate": "{{rate}}kHz audio",
"audioCodecRate": "{{codec}}{{rate}}kHz",
"noRecordings": "Sin grabaciones en este rango de tiempo"
}
"cameraOff": "La cámara está apagada"
}
-19
View File
@@ -496,9 +496,6 @@
"annotation_offset": {
"label": "Desplazamiento de anotaciones",
"description": "Milisegundos para desplazar las anotaciones de detección y alinear mejor los cuadros delimitadores de la línea de tiempo con las grabaciones; puede ser positivo o negativo."
},
"scene": {
"label": "Detectar escena"
}
},
"record": {
@@ -602,19 +599,6 @@
"enabled_in_config": {
"label": "Estado de grabación original",
"description": "Indica si la grabación estaba habilitada en la configuración estática original."
},
"sub": {
"continuous": {
"days": {
"label": "Días de retención",
"description": "Días a retener las grabaciones."
}
},
"motion": {
"days": {
"label": "Días de retención"
}
}
}
},
"ui": {
@@ -751,9 +735,6 @@
"order": {
"label": "Posición",
"description": "Posición numérica que controla el orden de la cámara en el diseño de Birdseye."
},
"modes": {
"label": "Tipos de actividad"
}
},
"ffmpeg": {
+1 -20
View File
@@ -711,9 +711,6 @@
"annotation_offset": {
"label": "Desplazamiento de anotaciones",
"description": "Milisegundos para desplazar las anotaciones de detección y alinear mejor los cuadros delimitadores de la línea de tiempo con las grabaciones; puede ser positivo o negativo."
},
"scene": {
"label": "Detectar escena"
}
},
"record": {
@@ -817,20 +814,7 @@
"label": "Estado de grabación original",
"description": "Indica si la grabación estaba habilitada en la configuración estática original."
},
"description": "Ajustes de grabación y retención aplicados a las cámaras salvo que se sobrescriban por cámara.",
"sub": {
"continuous": {
"days": {
"label": "Días de retención",
"description": "Días a retener las grabaciones."
}
},
"motion": {
"days": {
"label": "Días de retención"
}
}
}
"description": "Ajustes de grabación y retención aplicados a las cámaras salvo que se sobrescriban por cámara."
},
"camera_ui": {
"dashboard": {
@@ -1054,9 +1038,6 @@
"idle_heartbeat_fps": {
"label": "FPS de latido en reposo",
"description": "Fotogramas por segundo para reenviar el último fotograma compuesto de Birdseye en reposo; establécelo en 0 para deshabilitarlo."
},
"modes": {
"label": "Tipos de actividad"
}
},
"ffmpeg": {
@@ -578,6 +578,7 @@
"desc": "Frigate puede enviar notificaciones push a tu dispositivo de forma nativa cuando se ejecuta en el navegador o está instalado como una PWA."
},
"notificationUnavailable": {
"title": "Notificaciones no disponibles",
"desc": "Las notificaciones push web requieren un contexto seguro (<code>https://…</code>). Esto es una limitación del navegador. Accede a Frigate de forma segura para usar las notificaciones.",
"descPwa": "En iOS, las notificaciones push web solo están disponibles cuando Frigate está instalado en la pantalla de inicio. Abre el menú <strong>Compartir</strong>, selecciona <strong>Añadir a la pantalla de inicio</strong> y, a continuación, abre Frigate desde el nuevo icono para registrar este dispositivo para las notificaciones."
},
@@ -829,6 +830,7 @@
}
},
"semanticSearch": {
"title": "Búsqueda semántica desactivada",
"desc": "Búsqueda semántica debe estar activada para usar Disparadores."
},
"toast": {
+2
View File
@@ -254,6 +254,8 @@
"averageInf": "Tiempo promedio de inferencia"
},
"stats": {
"ffmpegHighCpuUsage": "{{camera}} tiene un uso elevado de CPU por FFmpeg ({{ffmpegAvg}}%)",
"detectHighCpuUsage": "{{camera}} tiene un uso elevado de CPU por detección ({{detectAvg}}%)",
"healthy": "El sistema está saludable",
"reindexingEmbeddings": "Reindexando empotrados ({{processed}}% completado)",
"detectIsSlow": "{{detect}} es lento ({{speed}} ms)",
+3 -34
View File
@@ -21,12 +21,7 @@
"lowBandwidthMode": "Väikese ribalaiusega režiim",
"twoWayTalk": {
"enable": "Lülita kahepoolne kõneside sisse",
"disable": "Lülita kahepoolne kõneside välja",
"requiresWebRTC": "Kahepoolne suhtlus eeldab WebRTC kasutamist, aga see poel saadaval",
"error": {
"microphone": "Kahepoolsel suhtlusel puudub ligipääs sinu mikrofonile",
"refused": "Kahepoolse suhtluse käivitamine ei õnnestunud. Lisateavet leiad veebibrauseri konsoolist."
}
"disable": "Lülita kahepoolne kõneside välja"
},
"cameraAudio": {
"enable": "Lülita kaamera heli sisse",
@@ -117,36 +112,10 @@
"title": "Voogedastus",
"lowBandwidth": {
"resetStream": "Lähtesta voogedastus",
"tips": "Reaalaja pilt on puhverdamise või voogedastuse vigade tõttu madala ribalaiusega režiimis.",
"force": {
"label": "Kasuta sundkorras väikese ribalaiusega režiimi",
"desc": "Valitud voogedastuse asemel esita sisu alati Frigate'i sisseehitatud väikese ribalaiusega voogedastusena. Toimib igasuguste ühendustega, aga pildikvaliteet on kehvem ning heliriba pole kasutusel."
}
"tips": "Reaalaja pilt on puhverdamise või voogedastuse vigade tõttu madala ribalaiusega režiimis."
},
"debug": {
"picker": "Voogedastuse osa valik pole silumisrežiimis saadaval. Silumisvaade kasutab alati voogedastust, millele on määratud tuvastamisroll.",
"technology": "Voogedastuse tehnoloogia valik pole silumisrežiimis saadaval."
},
"mode": "Voogedastuse tehnoloogia",
"technology": {
"description": "Vali eelistatud voogedastuse tehnoloogia. Taasesituse ja võrguvigade puhul võib Frigate tagavaravariandina kasutada väikese ribalaiusega režiimi.",
"name": {
"mse": "MSE",
"webrtc": "WebRTC",
"jsmpeg": "JSMpeg"
},
"tips": {
"mse": "Soovitatud vaikimisi väärtus, millel on lai ühilduvus ja sujuv taasesitus",
"webrtc": "Vajab täiendavat seadistust ja pole igas seadmes toetatud"
},
"unavailable": {
"browser": "Sinu veebibrauseris puudub WebRTC tugi.",
"not-configured": "WebRTC pole seadistatud. Seadista go2rtc webrtc kandidaadid või ice_server teenused.",
"unreachable": "WebRTC ühendamine ei õnnestunud. Kontrolli, et port 8555 on kättesaadav ja STUN/TURN-serveri seadistus on õige.",
"video-codec": "Selles veebibrauseris puudub WebRTC jaoks antud voogedastuse videokoodeki tugi.",
"audio-codec": "WebRTC ei toeta selle voogedastuse helikoodekit. Kodeeri go2rtc abil opus või G.711 vormingusse.",
"checking": "Kontrollin WebRTC saadavust…"
}
"picker": "Voogedastuse osa valik pole silumisrežiimis saadaval. Silumisvaade kasutab alati voogedastust, millele on määratud tuvastamisroll."
}
},
"notifications": "Teavitused",
@@ -1102,6 +1102,7 @@
"desc": "Frigate می‌تواند به‌صورت بومی وقتی در مرورگر اجرا می‌شود یا به‌عنوان PWA نصب شده است، اعلان‌های پوش را به دستگاه شما ارسال کند."
},
"notificationUnavailable": {
"title": "اعلان‌ها در دسترس نیستند",
"desc": "اعلان‌های پوش وب نیاز به یک بستر امن دارند ( <code>https://… </code>). این محدودیت مرورگر است. برای استفاده از اعلان‌ها، به‌صورت امن به Frigate دسترسی پیدا کنید.",
"descPwa": "در iOS، اعلان‌های وب پوش فقط زمانی در دسترس هستند که فریگیت روی صفحه اصلی شما نصب شده باشد. منوی <strong>اشتراک‌گذاری</strong> را باز کنید، <strong>افزودن به صفحه اصلی</strong> را انتخاب کنید، سپس فریگیت را از آیکون جدید باز کنید تا این دستگاه برای اعلان‌ها ثبت شود."
},
@@ -1209,6 +1210,7 @@
"triggers": {
"documentTitle": "تریگرها",
"semanticSearch": {
"title": "جستجوی معنایی غیرفعال است",
"desc": "برای استفاده از تریگرها باید جستجوی معنایی فعال باشد."
},
"management": {
+2
View File
@@ -226,6 +226,8 @@
}
},
"stats": {
"ffmpegHighCpuUsage": "{{camera}} استفادهٔ CPU بالایی برای FFmpeg دارد ({{ffmpegAvg}}%)",
"detectHighCpuUsage": "{{camera}} استفادهٔ CPU بالایی برای تشخیص دارد ({{detectAvg}}%)",
"reindexingEmbeddings": "بازتولید نمایهٔ embeddingها ({{processed}}% تکمیل شده)",
"cameraIsOffline": "{{camera}} آفلاین است",
"detectIsVerySlow": "{{detect}} بسیار کند است ({{speed}} ms)",
@@ -181,6 +181,7 @@
"desc": "Frigate peut envoyer nativement des notifications push à votre appareil lorsqu'il est exécuté dans le navigateur ou installé en tant que PWA."
},
"notificationUnavailable": {
"title": "Notifications indisponibles",
"desc": "Les notifications push Web nécessitent un contexte sécurisé (<code>https://…</code>). Il s'agit d'une limitation du navigateur. Accédez à Frigate en toute sécurité pour utiliser les notifications."
},
"globalSettings": {
@@ -796,6 +797,7 @@
}
},
"semanticSearch": {
"title": "La recherche sémantique est désactivée",
"desc": "La recherche sémantique doit être activée pour utiliser les déclencheurs."
},
"wizard": {
+2
View File
@@ -227,6 +227,8 @@
},
"lastRefreshed": "Dernier rafraichissement : ",
"stats": {
"ffmpegHighCpuUsage": "{{camera}} a un taux élevé d'utilisation processeur par FFmpeg ({{ffmpegAvg}}%)",
"detectHighCpuUsage": "{{camera}} : charge CPU détection élevée ({{detectAvg}}%)",
"healthy": "Le système est sain",
"reindexingEmbeddings": "Réindexation des embeddings ({{processed}} % terminée)",
"cameraIsOffline": "{{camera}} est hors ligne",
@@ -531,6 +531,7 @@
"desc": "Frigate יכולה לשלוח התראות דחיפה באופן טבעי למכשיר שלך כאשר הוא פועל בדפדפן או מותקן כ-PWA."
},
"notificationUnavailable": {
"title": "התראות לא זמינות",
"desc": "התראות דחיפה באינטרנט דורשות קישור מאובטח (<code>https://…</code>). זוהי מגבלה של הדפדפן. יש לגשת ל-Frigate בצורה מאובטחת כדי להשתמש בהתראות."
},
"globalSettings": {
@@ -948,6 +949,7 @@
"triggers": {
"documentTitle": "טריגרים",
"semanticSearch": {
"title": "חיפוש סמנטי מושבת",
"desc": "כדי להשתמש בטריגרים, יש להפעיל חיפוש סמנטי."
},
"management": {
+2
View File
@@ -1,6 +1,8 @@
{
"lastRefreshed": "רענון אחרון: ",
"stats": {
"ffmpegHighCpuUsage": "ל-{{camera}} יש צריכת מעבד גבוהה של FFmpeg ({{ffmpegAvg}}%)",
"detectHighCpuUsage": "ל-{{camera}} יש צריכת CPU גבוהה ({{detectAvg}}%)",
"healthy": "המערכת פועלת בצורה תקינה",
"reindexingEmbeddings": "אינדקס מחדש של ההטמעות ({{processed}}% הושלם)",
"cameraIsOffline": "{{camera}} לא זמינה",
@@ -855,6 +855,7 @@
"desc": "Frigate može nativno slati push obavijesti na vaš uređaj kada je pokrenut u pregledniku ili instaliran kao PWA."
},
"notificationUnavailable": {
"title": "Obavijesti nisu dostupne",
"desc": "Web push obavijesti zahtijevaju siguran kontekst (<code>https://...</code>). Ovo je ograničenje preglednika. Pristupite Frigateu sigurno kako biste koristili obavijesti."
},
"globalSettings": {
@@ -945,6 +946,7 @@
"triggers": {
"documentTitle": "Okidači",
"semanticSearch": {
"title": "Semantičko pretraživanje je onemogućeno",
"desc": "Semantičko pretraživanje mora biti omogućeno za korištenje okidača."
},
"management": {
+2
View File
@@ -164,6 +164,8 @@
},
"lastRefreshed": "Zadnje osvježavanje: ",
"stats": {
"ffmpegHighCpuUsage": "{{camera}} ima visoku upotrebu CPU-a za FFmpeg ({{ffmpegAvg}}%)",
"detectHighCpuUsage": "{{camera}} ima visoku upotrebu CPU-a za detekciju ({{detectAvg}}%)",
"healthy": "Sustav je zdrav",
"reindexingEmbeddings": "Reindeksiranje embeddings ({{processed}}% završeno)",
"cameraIsOffline": "{{camera}} je offline",
@@ -210,6 +210,7 @@
}
},
"notificationUnavailable": {
"title": "Értesítés elérhetetlen",
"desc": "A webes push értesítésekhez biztonságos környezet (<code>https://…</code>) szükséges. Ez egy böngészői korlátozás. A értesítések használatához férjen hozzá biztonságosan a Frigate-hez."
},
"sendTestNotification": "Teszt értesítés küldése",
@@ -710,6 +711,7 @@
}
},
"semanticSearch": {
"title": "Szemantikus keresés le van tiltva",
"desc": "A Triggerek használatához engedélyezni kell a szemantikus keresést."
},
"wizard": {
+3 -1
View File
@@ -201,7 +201,9 @@
"healthy": "A rendszer egészséges",
"cameraIsOffline": "{{camera}} nem elérhető",
"detectIsSlow": "{{detect}} lassú ({{speed}} ms)",
"reindexingEmbeddings": "Beágyazások újra indexelése ({{processed}}% kész)"
"reindexingEmbeddings": "Beágyazások újra indexelése ({{processed}}% kész)",
"ffmpegHighCpuUsage": "{{camera}}-nak/-nek magas FFmpeg CPU felhasználása ({{ffmpegAvg}}%)",
"detectHighCpuUsage": "A(z) {{camera}} kameránál magas az észlelési CPU-használat ({{detectAvg}}%)"
},
"lastRefreshed": "Utoljára frissítve: "
}
+4 -40
View File
@@ -6,12 +6,7 @@
"lowBandwidthMode": "Mode bandwidth rendah",
"twoWayTalk": {
"enable": "Aktifkan Audio Dua Arah",
"disable": "Nonaktifkan Audio Dua Arah",
"requiresWebRTC": "Percakapan dua arah memerlukan WebRTC, yang tidak tersedia",
"error": {
"microphone": "Percakapan dua arah tidak dapat mengakses mikrofon Anda",
"refused": "Percakapan dua arah gagal dimulai. Lihat konsol browser untuk detail."
}
"disable": "Nonaktifkan Audio Dua Arah"
},
"cameraAudio": {
"enable": "Aktifkan Audio Kamera",
@@ -136,8 +131,7 @@
"unavailable": "Audio tidak tersedia untuk stream ini"
},
"debug": {
"picker": "Pemilihan stream tidak tersedia dalam mode debug. Tampilan debug selalu menggunakan stream yang ditetapkan ke peran detect.",
"technology": "Pemilihan teknologi streaming tidak tersedia dalam mode debug."
"picker": "Pemilihan stream tidak tersedia dalam mode debug. Tampilan debug selalu menggunakan stream yang ditetapkan ke peran detect."
},
"twoWayTalk": {
"tips": "Perangkat Anda harus mendukung fitur ini dan WebRTC harus dikonfigurasi untuk audio dua arah.",
@@ -146,36 +140,11 @@
},
"lowBandwidth": {
"tips": "Tampilan live berada dalam mode bandwidth rendah karena buffering atau kesalahan stream.",
"resetStream": "Atur ulang stream",
"force": {
"label": "Paksa mode bandwidth rendah",
"desc": "Selalu putar feed bandwidth rendah bawaan Frigate, bukan stream yang dipilih. Berfungsi pada semua koneksi, tetapi kualitasnya lebih rendah dan tanpa audio."
}
"resetStream": "Atur ulang stream"
},
"playInBackground": {
"label": "Putar di latar belakang",
"tips": "Aktifkan opsi ini untuk melanjutkan streaming saat pemutar disembunyikan."
},
"mode": "Teknologi Streaming",
"technology": {
"description": "Pilih teknologi streaming yang Anda inginkan. Frigate mungkin tetap beralih ke mode bandwidth rendah jika terjadi kesalahan pemutaran atau jaringan.",
"name": {
"mse": "MSE",
"webrtc": "WebRTC",
"jsmpeg": "JSMpeg"
},
"tips": {
"mse": "Default yang direkomendasikan, kompatibilitas luas dan pemutaran lancar",
"webrtc": "Memerlukan pengaturan tambahan dan tidak didukung pada setiap perangkat"
},
"unavailable": {
"browser": "Browser Anda tidak mendukung WebRTC.",
"not-configured": "WebRTC belum dikonfigurasi. Atur candidates WebRTC go2rtc atau ice_servers.",
"unreachable": "WebRTC tidak dapat terhubung. Pastikan port 8555 dapat dijangkau dan server STUN/TURN telah dikonfigurasi dengan benar.",
"video-codec": "Codec video stream ini tidak didukung oleh WebRTC pada browser ini.",
"audio-codec": "Codec audio stream ini tidak didukung oleh WebRTC. Lakukan transcoding ke Opus atau G.711 menggunakan go2rtc.",
"checking": "Memeriksa ketersediaan WebRTC…"
}
}
},
"cameraSettings": {
@@ -185,8 +154,7 @@
"snapshots": "Snapshot",
"audioDetection": "Deteksi Audio",
"transcription": "Transkripsi Audio",
"autotracking": "Pelacakan Otomatis",
"camera": "Kamera"
"autotracking": "Pelacakan Otomatis"
},
"history": {
"label": "Tampilkan rekaman historis"
@@ -223,9 +191,5 @@
"description": "Grup kamera ini tidak memiliki kamera yang ditetapkan atau diaktifkan.",
"buttonText": "Kelola Grup"
}
},
"camera": {
"turnOn": "Nyalakan Kamera",
"turnOff": "Matikan Kamera"
}
}
+29 -184
View File
@@ -58,7 +58,7 @@
"systemTelemetry": "Telemetri",
"systemBirdseye": "Birdseye",
"systemFfmpeg": "FFmpeg",
"systemDetectorsAndModel": "Model deteksi",
"systemDetectorsAndModel": "Detektor dan model",
"systemMqtt": "MQTT",
"systemGo2rtcStreams": "Stream go2rtc",
"integrationSemanticSearch": "Pencarian semantik",
@@ -68,7 +68,7 @@
"integrationObjectClassification": "Klasifikasi objek",
"integrationAudioTranscription": "Transkripsi audio",
"cameraDetect": "Deteksi objek",
"cameraFfmpeg": "Stream (FFmpeg)",
"cameraFfmpeg": "FFmpeg",
"cameraRecording": "Perekaman",
"cameraSnapshots": "Cuplikan",
"cameraMotion": "Deteksi gerakan",
@@ -150,48 +150,17 @@
"toast": {
"success": {
"clearStoredLayout": "Tata letak tersimpan untuk {{cameraName}} telah dihapus",
"clearStreamingSettings": "Pengaturan streaming untuk semua grup kamera telah dihapus.",
"exportUiSettings": "Pengaturan diekspor",
"importUiSettings": "Pengaturan diimpor"
"clearStreamingSettings": "Pengaturan streaming untuk semua grup kamera telah dihapus."
},
"error": {
"clearStoredLayoutFailed": "Gagal menghapus tata letak tersimpan: {{errorMessage}}",
"clearStreamingSettingsFailed": "Gagal menghapus pengaturan streaming: {{errorMessage}}",
"exportUiSettingsFailed": "Gagal mengekspor pengaturan",
"importNothingToApply": "Gagal mengimpor pengaturan: file tidak berisi pengaturan",
"importInvalidJson": "Gagal mengimpor pengaturan: file bukan JSON yang valid",
"importWrongType": "Gagal mengimpor pengaturan: file bukan ekspor pengaturan Frigate",
"importUnsupportedVersion": "Gagal mengimpor pengaturan: file memerlukan versi Frigate yang lebih baru",
"importInvalidSchema": "Gagal mengimpor pengaturan: file tidak valid",
"importUiSettingsFailed": "Gagal mengimpor pengaturan"
}
},
"backupRestore": {
"title": "Pencadangan & Pemulihan",
"transfer": {
"label": "File Pengaturan Perangkat",
"desc": "Tata letak grup kamera, pengaturan streaming, dan preferensi UI disimpan di browser Anda. Ekspor ke file untuk mencadangkannya atau memindahkannya ke perangkat lain.",
"export": "Ekspor Pengaturan",
"import": "Impor Pengaturan"
},
"importDialog": {
"title": "Impor Pengaturan",
"desc": "Pilih yang akan diterapkan dari file ini. Frigate akan dimuat ulang setelah impor selesai.",
"exportedFrom": "Diekspor pada {{date}} dari konfigurasi Frigate versi {{version}}",
"layouts_other": "Tata letak grup kamera ({{count}} grup)",
"streaming_other": "Pengaturan streaming ({{count}} kamera)",
"preferences_other": "Preferensi UI ({{count}} pengaturan)",
"unknownGroups_other": "Grup kamera {{groups}} tidak ada di server ini. Pengaturannya akan disimpan tetapi tidak digunakan.",
"unknownCameras_other": "Kamera {{cameras}} tidak ada di server ini. Pengaturan streamingnya akan diabaikan.",
"confirm": "Impor"
"clearStreamingSettingsFailed": "Gagal menghapus pengaturan streaming: {{errorMessage}}"
}
}
},
"configMessages": {
"audioTranscription": {
"audioDetectionDisabled": "Deteksi audio tidak diaktifkan untuk kamera ini. Transkripsi audio memerlukan deteksi audio yang aktif.",
"audioDetectionRuntimeDisabled": "Deteksi audio diaktifkan dalam konfigurasi Anda, tetapi saat ini dinonaktifkan untuk kamera ini, sehingga transkripsi audio tidak akan berjalan. Aktifkan kembali dari tampilan langsung kamera, atau periksa apakah profil aktif menonaktifkannya.",
"genaiProviderSelected": "Penyedia GenAI dipilih, sehingga pengaturan perangkat dan ukuran model diabaikan."
"audioDetectionDisabled": "Deteksi audio tidak diaktifkan untuk kamera ini. Transkripsi audio memerlukan deteksi audio yang aktif."
},
"detect": {
"fpsGreaterThanFive": "Mengatur FPS deteksi lebih dari 5 tidak direkomendasikan. Nilai yang lebih tinggi dapat menyebabkan masalah performa dan tidak akan memberikan manfaat apa pun.",
@@ -201,9 +170,7 @@
"maxFramesSet": "Menetapkan frame maksimum akan menimpa perilaku default dan menonaktifkan pelacakan objek diam. Hanya sedikit situasi yang memerlukan ini, gunakan dengan hati-hati.",
"squareResolution": "Resolusi deteksi berbentuk persegi tidak lazim. Lebar dan tinggi deteksi harus sesuai dengan rasio aspek kamera Anda, misalnya 16:9, bukan dimensi model deteksi objek. Rasio aspek yang tidak sesuai dapat meregangkan gambar dan mengurangi akurasi deteksi.",
"resolutionHigh": "Resolusi deteksi ini lebih tinggi daripada yang direkomendasikan dan dapat meningkatkan penggunaan sumber daya tanpa meningkatkan akurasi deteksi. Resolusi deteksi 1080p atau lebih rendah direkomendasikan untuk sebagian besar kamera.",
"globalResolutionMultipleCameras": "Resolusi deteksi global ditetapkan saat beberapa kamera dikonfigurasi. Kecuali semua kamera memiliki resolusi dan rasio aspek yang sama, lebar dan tinggi deteksi sebaiknya ditentukan per kamera agar sesuai dengan rasio aspek asli masing-masing kamera.",
"runtimeDisabled": "Deteksi objek diaktifkan dalam konfigurasi Anda, tetapi saat ini dinonaktifkan untuk kamera ini. Snapshot, item tinjauan, dan pengayaan seperti pengenalan wajah, pengenalan pelat nomor, serta Generative AI tidak akan berfungsi hingga fitur tersebut diaktifkan kembali dari tampilan langsung kamera, atau hingga profil aktif berhenti menonaktifkannya.",
"sceneWithoutModel": "Tidak ada model deteksi yang dikonfigurasi untuk scene ini, sehingga kamera ini menggunakan model dengan scene “All cameras” sebagai cadangan. Tambahkan model untuk scene ini agar kamera memiliki modelnya sendiri."
"globalResolutionMultipleCameras": "Resolusi deteksi global ditetapkan saat beberapa kamera dikonfigurasi. Kecuali semua kamera memiliki resolusi dan rasio aspek yang sama, lebar dan tinggi deteksi sebaiknya ditentukan per kamera agar sesuai dengan rasio aspek asli masing-masing kamera."
},
"faceRecognition": {
"globalDisabled": "Pengayaan pengenalan wajah harus diaktifkan agar fitur pengenalan wajah berfungsi pada kamera ini.",
@@ -212,17 +179,14 @@
},
"lpr": {
"globalDisabled": "Pengayaan pengenalan pelat nomor harus diaktifkan agar fitur LPR berfungsi pada kamera ini.",
"vehicleNotTracked": "Pengenalan pelat nomor memerlukan kendaraan untuk dilacak. Aktifkan “car” atau jenis kendaraan lain pada Objects untuk kamera ini.",
"vehicleNotTracked": "Pengenalan pelat nomor memerlukan 'car' atau 'motorcycle' untuk dilacak. Aktifkan 'car' atau 'motorcycle' di Objek untuk kamera ini.",
"modelSizeLarge": "Model 'large' dioptimalkan untuk pelat nomor multi-baris. Model 'small' memberikan performa lebih baik daripada 'large' dan sebaiknya digunakan kecuali wilayah Anda menggunakan format pelat multi-baris."
},
"review": {
"recordDisabled": "Perekaman dinonaktifkan, item tinjauan tidak akan dibuat.",
"detectDisabled": "Deteksi objek dinonaktifkan. Item tinjauan memerlukan objek yang terdeteksi untuk mengategorikan alert dan deteksi.",
"allNonAlertDetections": "Semua aktivitas non-alert akan disertakan sebagai deteksi.",
"genaiImageSourceRecordingsRecordDisabled": "Sumber gambar disetel ke 'rekaman', tetapi perekaman dinonaktifkan. Frigate akan kembali ke gambar pratinjau.",
"recordRuntimeDisabled": "Perekaman diaktifkan dalam konfigurasi Anda, tetapi saat ini dinonaktifkan untuk kamera ini, sehingga item tinjauan tidak akan dibuat. Aktifkan kembali dari tampilan langsung kamera, atau periksa apakah profil aktif menonaktifkannya.",
"detectRuntimeDisabled": "Deteksi objek diaktifkan dalam konfigurasi Anda, tetapi saat ini dinonaktifkan untuk kamera ini. Item tinjauan memerlukan objek yang terdeteksi untuk mengategorikan peringatan dan deteksi. Aktifkan kembali dari tampilan langsung kamera, atau periksa apakah profil aktif menonaktifkannya.",
"genaiImageSourceRecordingsRecordRuntimeDisabled": "Sumber gambar diatur ke “recordings”, tetapi perekaman saat ini dinonaktifkan untuk kamera ini meskipun konfigurasi Anda mengaktifkannya. Frigate akan menggunakan gambar pratinjau sebagai pengganti."
"genaiImageSourceRecordingsRecordDisabled": "Sumber gambar disetel ke 'rekaman', tetapi perekaman dinonaktifkan. Frigate akan kembali ke gambar pratinjau."
},
"audio": {
"noAudioRole": "Tidak ada stream yang memiliki peran audio yang didefinisikan. Anda harus mengaktifkan peran audio agar deteksi audio berfungsi."
@@ -231,30 +195,16 @@
"genaiNoDescriptionsProvider": "Anda harus mengonfigurasi penyedia GenAI dengan peran 'deskripsi' agar deskripsi dapat dibuat."
},
"record": {
"noRecordRole": "Tidak ada stream yang memiliki peran record yang didefinisikan. Perekaman tidak akan berfungsi.",
"noRecordSubRole": "Tidak ada stream yang memiliki peran record_sub. Perekaman sub stream tidak akan berfungsi."
"noRecordRole": "Tidak ada stream yang memiliki peran record yang didefinisikan. Perekaman tidak akan berfungsi."
},
"snapshots": {
"detectDisabled": "Deteksi objek dinonaktifkan. Cuplikan dihasilkan dari objek yang terlacak dan tidak akan dibuat.",
"detectRuntimeDisabled": "Deteksi objek diaktifkan dalam konfigurasi Anda, tetapi saat ini dinonaktifkan untuk kamera ini, sehingga snapshot tidak akan dibuat. Aktifkan kembali dari tampilan langsung kamera, atau periksa apakah profil aktif menonaktifkannya."
"detectDisabled": "Deteksi objek dinonaktifkan. Cuplikan dihasilkan dari objek yang terlacak dan tidak akan dibuat."
},
"semanticSearch": {
"jinav2SmallModelSize": "Ukuran 'small' dengan model Jina V2 memiliki RAM tinggi dan biaya inferensi. Model 'large' dengan GPU diskrit direkomendasikan."
},
"onvif": {
"autotrackingNoZones": "Pelacakan otomatis memerlukan setidaknya satu zona. Tentukan zona untuk kamera ini di Masker / Zona, lalu tetapkan sebagai zona wajib di bawah."
},
"model": {
"optimizedFor320": "Frigate dioptimalkan untuk model 320x320, yang merupakan pilihan terbaik untuk sebagian besar konfigurasi. Model 640x640 lebih lambat dan hanya membantu dalam skenario tertentu.",
"inputDimensionsNotDetectResolution": "Lebar dan tinggi input model adalah dimensi input model deteksi objek, bukan resolusi deteksi kamera Anda. Nilainya harus sesuai dengan dimensi model yang digunakan—biasanya ukuran persegi seperti 320x320 atau 640x640."
},
"ffmpeg": {
"hwaccelManualNotRecommended": "Argumen akselerasi perangkat keras manual tidak direkomendasikan. Kecuali terdapat kebutuhan khusus, pilih preset yang sesuai dengan perangkat keras Anda.",
"inputsMissingGo2rtcStream": "Input di bawah ini mengarah ke restream go2rtc yang sudah tidak ada. Pilih restream yang tersedia atau masukkan URL kamera secara manual; jika tidak, kamera ini akan gagal terhubung."
},
"birdseye": {
"objectTrackingDetectDisabled": "Birdseye mencakup objek yang dilacak, tetapi deteksi objek dinonaktifkan untuk kamera ini. Kamera tidak akan muncul di Birdseye.",
"objectTrackingDetectRuntimeDisabled": "Birdseye mencakup objek yang dilacak, tetapi deteksi objek saat ini dinonaktifkan untuk kamera ini meskipun konfigurasi Anda mengaktifkannya. Kamera tidak akan muncul di Birdseye hingga deteksi objek diaktifkan kembali dari tampilan langsung kamera, atau hingga profil aktif berhenti menonaktifkannya."
}
},
"button": {
@@ -272,10 +222,7 @@
"heading_other": "Bagian global ini memiliki bidang yang ditimpa di {{count}} kamera.",
"othersField_other": "{{count}} lainnya",
"profilePrefix": "Profil {{profile}}: {{fields}}"
},
"overriddenLive": "Ditimpa (Langsung)",
"overriddenLiveTooltip": "Kamera ini berjalan dengan nilai yang berbeda dari nilai yang tersimpan dalam konfigurasi Anda, yang ditampilkan di sini. Tampilan langsung, MQTT, atau profil aktif dapat mengubahnya saat Frigate berjalan.",
"overriddenLiveValue": "Nilai yang berjalan: {{value}}"
}
},
"menuDot": {
"overrideGlobal": "Bagian ini menimpa konfigurasi global",
@@ -460,7 +407,7 @@
}
},
"step3": {
"description": "Konfigurasikan fitur, peran stream, dan tambahkan stream tambahan untuk kamera Anda.",
"description": "Konfigurasikan peran stream dan tambahkan stream tambahan untuk kamera Anda.",
"streamsTitle": "Stream Kamera",
"addStream": "Tambah Stream",
"addAnotherStream": "Tambah Stream Lain",
@@ -494,26 +441,11 @@
"title": "Peran Stream",
"detect": "Umpan utama untuk deteksi objek.",
"record": "Menyimpan segmen umpan video berdasarkan pengaturan konfigurasi.",
"audio": "Umpan untuk deteksi berbasis audio.",
"record_sub": "Rekaman berkualitas lebih rendah untuk pemutaran adaptif dan retensi lebih lama (biasanya sub stream kamera)."
"audio": "Umpan untuk deteksi berbasis audio."
},
"featuresPopover": {
"title": "Fitur Stream",
"description": "Gunakan restreaming go2rtc untuk mengurangi koneksi ke kamera Anda."
},
"appleCompatibility": {
"title": "Tingkatkan pemutaran pada perangkat Apple",
"description": "Aktifkan jika Anda menonton rekaman melalui Safari atau perangkat iPhone, iPad, atau Mac."
},
"ptz": {
"title": "Aktifkan Kontrol PTZ",
"detectedNote": "Dukungan PTZ telah terdeteksi melalui ONVIF. Jika ini adalah kamera PTZ, mengaktifkan opsi ini memungkinkan Anda mengendalikan fungsi pan/tilt/zoom kamera dari UI.",
"connectionDetails": "Detail koneksi ONVIF",
"host": "Host ONVIF",
"port": "Port ONVIF",
"username": "Nama Pengguna ONVIF",
"password": "Kata Sandi ONVIF",
"hostRequiredWarning": "Host dan port ONVIF diperlukan saat kontrol PTZ diaktifkan."
}
},
"step4": {
@@ -574,7 +506,7 @@
"deleteCameraDialog": {
"title": "Hapus Kamera",
"description": "Menghapus kamera akan menghapus secara permanen semua rekaman, objek terlacak, dan konfigurasi untuk kamera tersebut. Semua stream go2rtc yang terkait dengan kamera ini mungkin masih perlu dihapus secara manual.",
"selectPlaceholder": "Pilih kamera…",
"selectPlaceholder": "Pilih kamera...",
"confirmTitle": "Apakah Anda yakin?",
"confirmWarning": "Menghapus <strong>{{cameraName}}</strong> tidak dapat dibatalkan.",
"deleteExports": "Juga hapus ekspor untuk kamera ini",
@@ -610,7 +542,7 @@
"webuiUrlHelp": "URL untuk membuka web UI kamera langsung dari tampilan Debug. Biarkan kosong untuk menonaktifkan tautan.",
"webuiUrlInvalid": "Harus berupa URL yang valid (misalnya, https://example.com).",
"dashboardLabel": "Tampilkan di dasbor Live",
"dashboardHelp": "Tampilkan kamera ini pada dasbor tampilan langsung Semua Kamera secara default. Kamera tetap tersedia di semua tempat lain, termasuk grup kamera.",
"dashboardHelp": "Tampilkan kamera ini di dasbor Live.",
"reviewLabel": "Tampilkan di Review",
"reviewHelp": "Tampilkan kamera ini di Review, termasuk filter kamera, review gerakan, dan tampilan riwayat."
}
@@ -1157,7 +1089,7 @@
},
"createUser": {
"title": "Buat Pengguna Baru",
"desc": "Tambahkan akun pengguna baru dan tentukan peran untuk akses ke area UI Frigate.",
"desc": "Tambahkan akun pengguna baru dan tentukan perannya untuk akses ke area UI Frigate.",
"usernameOnlyInclude": "Nama pengguna hanya boleh berisi huruf, angka, . atau _",
"confirmPassword": "Harap konfirmasi kata sandi Anda"
},
@@ -1260,8 +1192,9 @@
"desc": "Frigate dapat secara native mengirim notifikasi push ke perangkat Anda saat berjalan di browser atau diinstal sebagai PWA."
},
"notificationUnavailable": {
"desc": "Notifikasi push web memerlukan konteks aman ([https://…](https://…)). Ini merupakan batasan browser. Akses Frigate secara aman untuk menggunakan notifikasi.",
"descPwa": "Di iOS, notifikasi push web hanya tersedia jika Frigate dipasang ke Layar Utama. Buka menu Bagikan, pilih Tambahkan ke Layar Utama, lalu buka Frigate dari ikon baru untuk mendaftarkan perangkat ini untuk notifikasi."
"title": "Notifikasi Tidak Tersedia",
"desc": "Notifikasi push web memerlukan konteks aman (<code>https://…</code>). Ini adalah batasan browser. Akses Frigate secara aman untuk menggunakan notifikasi.",
"descPwa": "Di iOS, notifikasi push web hanya tersedia jika Frigate dipasang ke Layar Utama Anda. Buka menu <strong>Bagikan</strong>, pilih <strong>Tambahkan ke Layar Utama</strong>, lalu buka Frigate dari ikon baru tersebut untuk mendaftarkan perangkat ini agar menerima notifikasi."
},
"globalSettings": {
"title": "Pengaturan Global",
@@ -1367,6 +1300,7 @@
"triggers": {
"documentTitle": "Pemicu",
"semanticSearch": {
"title": "Pencarian Semantik dinonaktifkan",
"desc": "Pencarian Semantik harus diaktifkan untuk menggunakan Pemicu."
},
"management": {
@@ -1524,9 +1458,7 @@
"orphansDeleted": "File Yatim Piatu Dihapus",
"aborted": "Dibatalkan. Penghapusan akan melebihi ambang keamanan.",
"error": "Kesalahan",
"totals": "Total",
"spaceToReclaim": "Ruang yang Dapat Diklaim Kembali",
"spaceReclaimed": "Ruang Berhasil Diklaim Kembali"
"totals": "Total"
},
"event_snapshots": "Cuplikan Objek Terlacak",
"event_thumbnails": "Thumbnail Objek Terlacak",
@@ -1568,23 +1500,11 @@
"keyLabel": "Kunci",
"valueLabel": "Nilai",
"keyPlaceholder": "Kunci baru",
"remove": "Hapus",
"providerNameLabel": "Nama penyedia",
"providerNamePlaceholder": "mis., openai",
"variableNameLabel": "Nama variabel",
"variableNamePlaceholder": "mis., MY_VARIABLE",
"loggerNameLabel": "Nama logger",
"loggerNamePlaceholder": "mis., frigate.record",
"keyPatternError": "Gunakan hanya huruf, angka, tanda hubung, dan garis bawah (tanpa spasi)"
"remove": "Hapus"
},
"knownPlates": {
"namePlaceholder": "mis., Mobil Istri",
"platePlaceholder": "Nomor pelat atau regex",
"assignedTo": "Ditugaskan kepada {{name}}",
"detected": "Pelat nomor terdeteksi",
"noneDetected": "Belum ada pelat nomor yang terdeteksi",
"search": "Cari atau masukkan pelat nomor",
"useCustom": "Gunakan \"{{value}}\""
"platePlaceholder": "Nomor pelat atau regex"
},
"timezone": {
"defaultOption": "Gunakan zona waktu browser"
@@ -1629,21 +1549,10 @@
"preset-record-mjpeg": "Rekam - Kamera MJPEG",
"preset-record-jpeg": "Rekam - Kamera JPEG",
"preset-record-ubiquiti": "Rekam - Kamera Ubiquiti"
},
"sameAsRecord": "Sama seperti argumen keluaran rekaman"
}
},
"cameraInputs": {
"itemTitle": "Stream {{index}}",
"sourceMode": {
"restream": "Restream (go2rtc)",
"manual": "Jalur input manual",
"go2rtcStreamLabel": "Stream go2rtc",
"go2rtcStreamPlaceholder": "Pilih stream go2rtc",
"noGo2rtcStreams": "Tidak ada stream go2rtc yang dikonfigurasi",
"go2rtcStreamSearch": "Cari stream...",
"availableStreams": "Stream yang tersedia",
"noMatchingStreams": "Tidak ada stream yang sesuai"
}
"itemTitle": "Stream {{index}}"
},
"restartRequiredField": "Perlu mulai ulang",
"restartRequiredFooter": "Konfigurasi berubah - Perlu mulai ulang",
@@ -1715,18 +1624,14 @@
"options": {
"detect": "Deteksi",
"record": "Rekam",
"audio": "Audio",
"record_sub": "Rekam (Sub Stream)"
},
"roleInUse": "Sudah ditetapkan ke stream lain",
"recordSubConflict": "Sebuah stream tidak dapat memiliki peran record dan record_sub sekaligus"
"audio": "Audio"
}
},
"genaiRoles": {
"options": {
"embeddings": "Embedding",
"descriptions": "Deskripsi",
"chat": "Chat",
"transcribe": "Transkripsi"
"chat": "Chat"
}
},
"semanticSearchModel": {
@@ -1784,26 +1689,7 @@
},
"defaultRole": {
"admin": "Admin",
"viewer": "Penampil",
"none": "Tidak ada (tolak akses)"
},
"birdseyeModes": {
"summary": "{{count}} dipilih",
"options": {
"continuous": "Kontinu",
"motion": "Gerakan",
"all_objects": "Semua objek",
"alerts": "Peringatan",
"detections": "Deteksi"
}
},
"audioTranscriptionModel": {
"placeholder": "Pilih model…",
"builtIn": "Model Bawaan",
"genaiProviders": "Penyedia GenAI"
},
"audioTranscriptionModelSize": {
"notApplicable": "Tidak berlaku untuk penyedia GenAI"
"viewer": "Penampil"
}
},
"globalConfig": {
@@ -1970,10 +1856,6 @@
"imageSource": {
"recordings": "Rekaman",
"previews": "Pratinjau"
},
"frameMode": {
"frames": "Frame",
"annotated_frames": "Frame beranotasi"
}
},
"logger": {
@@ -1999,42 +1881,5 @@
"modelSize": {
"small": "Kecil",
"large": "Besar"
},
"detectionModels": {
"title": "Model deteksi",
"description": "Konfigurasikan model deteksi objek dan perangkat keras yang digunakan oleh setiap model. Kamera memilih model berdasarkan scene dalam pengaturan deteksi kamera.",
"addModel": "Tambahkan model",
"cameras_other": "{{count}} kamera",
"scene": {
"label": "Scene",
"description": "Lingkungan yang menjadi tujuan model ini. Kamera memilih model dengan menetapkan scene yang sama dalam pengaturan deteksinya, sedangkan model dengan scene “all” digunakan oleh setiap kamera yang tidak menetapkan scene."
},
"scenes": {
"all": "Semua kamera",
"indoor": "Dalam ruangan",
"outdoor": "Luar ruangan",
"indoor_thermal": "Termal dalam ruangan",
"outdoor_thermal": "Termal luar ruangan"
},
"hardware": {
"label": "Perangkat keras",
"placeholder": "Pilih perangkat keras",
"loading": "Mencari perangkat keras deteksi...",
"none": "Tidak ada perangkat keras yang dipilih",
"claimedBy": "digunakan oleh {{scene}}",
"detectorCount": "Detektor",
"countRecommended_other": "Direkomendasikan untuk {{count}} kamera",
"unrecognized": "Model ini dikonfigurasi untuk perangkat keras yang tidak ditemukan pada sistem ini: {{devices}}",
"description": "Perangkat keras tempat model ini menjalankan proses deteksi.",
"detectorCountDescription": "Jumlah proses deteksi yang dijalankan pada perangkat keras ini. Lebih banyak detektor dapat menangani lebih banyak kamera, dengan konsekuensi penggunaan memori perangkat tambahan.",
"unitsDescription": "Setiap unit menjalankan proses deteksinya sendiri. Unit yang sudah digunakan oleh model lain tidak dapat dipilih."
},
"tabs": {
"plus": "Frigate+",
"custom": "Model Kustom"
},
"plusModel": {
"noModelSelected": "Pilih model Frigate+"
}
}
}
+2
View File
@@ -214,6 +214,8 @@
},
"lastRefreshed": "Terakhir diperbarui: ",
"stats": {
"ffmpegHighCpuUsage": "{{camera}} memiliki penggunaan CPU FFmpeg yang tinggi ({{ffmpegAvg}}%)",
"detectHighCpuUsage": "{{camera}} memiliki penggunaan CPU deteksi yang tinggi ({{detectAvg}}%)",
"healthy": "Sistem sehat",
"reindexingEmbeddings": "Mengindeks ulang embedding ({{processed}}% selesai)",
"cameraIsOffline": "{{camera}} sedang offline",
@@ -661,6 +661,7 @@
"registerDevice": "Registra questo dispositivo",
"notificationUnavailable": {
"desc": "Le notifiche push web richiedono un contesto sicuro (<code>https://...</code>). Questa è una limitazione del browser. Accedi a Frigate in modo sicuro per utilizzare le notifiche.",
"title": "Notifiche non disponibili",
"descPwa": "Su iOS, le notifiche push web sono disponibili solo se Frigate è installato sulla schermata Home. Apri il menu <strong>Condividi</strong>, scegli <strong>Aggiungi alla schermata Home</strong>, quindi apri Frigate dalla nuova icona per registrare questo dispositivo per le notifiche."
},
"deviceSpecific": "Impostazioni specifiche del dispositivo",
@@ -841,6 +842,7 @@
}
},
"semanticSearch": {
"title": "La ricerca semantica è disabilitata",
"desc": "Per utilizzare gli attivatori, è necessario abilitare la ricerca semantica."
},
"wizard": {
+2
View File
@@ -222,6 +222,8 @@
}
},
"stats": {
"detectHighCpuUsage": "{{camera}} ha un utilizzo elevato della CPU con il rilevamento ({{detectAvg}}%)",
"ffmpegHighCpuUsage": "{{camera}} ha un elevato utilizzo della CPU con FFmpeg ({{ffmpegAvg}}%)",
"healthy": "Il sistema è integro",
"reindexingEmbeddings": "Reindicizzazione degli incorporamenti (completata al {{processed}}%)",
"cameraIsOffline": "{{camera}} è disconnessa",
+3 -1
View File
@@ -691,6 +691,7 @@
"desc": "Frigate はブラウザで実行中、または PWA としてインストールされている場合に、端末へネイティブのプッシュ通知を送信できます。"
},
"notificationUnavailable": {
"title": "通知は利用できません",
"desc": "Web プッシュ通知にはセキュアコンテキスト(<code>https://…</code>)が必要です。これはブラウザの制限です。通知を利用するには、セキュアに Frigate へアクセスしてください。",
"descPwa": "iOSでは、Frigateをホーム画面に追加した場合にのみ、Webプッシュ通知を利用できます。<strong>共有</strong>メニューを開き、<strong>ホーム画面に追加</strong>を選択してから、新しいアイコンからFrigateを起動し、このデバイスを通知対象として登録してください。"
},
@@ -895,7 +896,8 @@
}
},
"semanticSearch": {
"desc": "トリガーを使用するにはセマンティック検索を有効にする必要があります。"
"desc": "トリガーを使用するにはセマンティック検索を有効にする必要があります。",
"title": "セマンティック検索が無効です"
},
"wizard": {
"title": "トリガーを作成",
+2
View File
@@ -227,6 +227,8 @@
},
"lastRefreshed": "最終更新: ",
"stats": {
"ffmpegHighCpuUsage": "{{camera}} の FFmpeg の CPU 使用率が高い({{ffmpegAvg}}%)",
"detectHighCpuUsage": "{{camera}} の検知の CPU 使用率が高い({{detectAvg}}%)",
"healthy": "システムは正常です",
"reindexingEmbeddings": "埋め込みを再インデックス中({{processed}}% 完了)",
"cameraIsOffline": "{{camera}} はオフラインです",
@@ -1143,6 +1143,7 @@
"desc": "Frigate អាចផ្ញើការជូនដំណឹង Push ទៅឧបករណ៍របស់អ្នកដោយផ្ទាល់ នៅពេលដែលវាកំពុងដំណើរការក្នុងកម្មវិធីរុករក ឬបានដំឡើងជា PWA។"
},
"notificationUnavailable": {
"title": "ការជូនដំណឹងមិនអាចប្រើបាន",
"desc": "ការជូនដំណឹង Push លើវេប ត្រូវការបរិយាកាសសុវត្ថិភាព (<code>https://…</code>)។ នេះគឺជាការកំណត់របស់កម្មវិធីរុករក។ ចូលប្រើ Frigate ដោយសុវត្ថិភាព ដើម្បីប្រើការជូនដំណឹង។",
"descPwa": "នៅលើ iOS ការជូនដំណឹង Push លើវេប អាចប្រើបានតែនៅពេលដែល Frigate ត្រូវបានដំឡើងទៅកាន់អេក្រង់ដើមរបស់អ្នក។ បើកម៉ឺនុយ <strong>ចែករំលែក</strong> ជ្រើសរើស <strong>បន្ថែមទៅអេក្រង់ដើម</strong> បន្ទាប់មកបើក Frigate ពីរូបតំណាងថ្មី ដើម្បីចុះឈ្មោះឧបករណ៍នេះសម្រាប់ការជូនដំណឹង។"
},
@@ -1250,6 +1251,7 @@
"triggers": {
"documentTitle": "កេះ",
"semanticSearch": {
"title": "ការស្វែងរកតាមអត្ថន័យត្រូវបានបិទ",
"desc": "ការស្វែងរកតាមអត្ថន័យត្រូវតែបើក ដើម្បីប្រើកេះ។"
},
"management": {
@@ -67,6 +67,7 @@
},
"documentTitle": "트리거",
"semanticSearch": {
"title": "시맨틱 검색이 비활성화됨",
"desc": "트리거 기능을 사용하려면 시맨틱 검색이 활성화되어 있어야 합니다."
},
"management": {
@@ -1307,6 +1308,7 @@
"desc": "Frigate이 브라우저에서 실행 중이거나 PWA로 설치된 경우 기기로 푸시 알림을 기본 전송할 수 있습니다."
},
"notificationUnavailable": {
"title": "알림을 사용할 수 없음",
"desc": "웹 푸시 알림을 사용하려면 보안 컨텍스트(<code>https://…</code>)가 필요합니다. 이는 브라우저 자체의 제한 사항입니다. 알림을 사용하려면 Frigate에 보안 연결로 접속하세요.",
"descPwa": "iOS의 경우 Frigate이 홈 화면에 추가되어 있어야만 웹 푸시 알림을 사용할 수 있습니다. <strong>공유</strong> 메뉴를 열고 <strong>홈 화면에 추가</strong>를 선택한 다음, 새로 생성된 아이콘으로 Frigate을 실행하여 기기를 알림에 등록하세요."
},
+10
View File
@@ -236,6 +236,8 @@
},
"lastRefreshed": "마지막 갱신: ",
"stats": {
"ffmpegHighCpuUsage": "{{camera}} 카메라의 FFmpeg CPU 사용량이 높습니다 ({{ffmpegAvg}}%)",
"detectHighCpuUsage": "{{camera}} 카메라의 감지(detect) CPU 사용량이 높습니다 ({{detectAvg}}%)",
"healthy": "시스템 상태가 양호합니다",
"reindexingEmbeddings": "임베딩 재색인 중 ({{processed}}% 완료)",
"cameraIsOffline": "{{camera}} 카메라가 오프라인 상태입니다",
@@ -279,12 +281,20 @@
"notices": {
"title": "알림",
"empty": "Frigate 이 잘 설치되어있습니다",
"dismiss": "무시",
"openSettings": "설정 열기",
"openLink": "링크 열기",
"noMatches": "필터에 맞는 알림이 없습니다",
"dismissedTitle": "무시됨",
"noneDismissed": "무시된 알림 없음",
"clearDismissed": "무시됨 지우기",
"clearDismissedTitle": "무시된 알림을 지울까요?",
"clearDismissedDesc": "무시된 모든 알림이 삭제됩니다. 설정 및 스트림 점검 항목은 즉시 다시 표시되며, 기타 알림은 다음에 다시 발생할 때 표시됩니다.",
"firstSeen_one": "최초 감지: {{time}}",
"firstSeen_other": "최초 감지: {{time}} · {{count}}회",
"dismissedAt": "무시됨: {{time}}",
"filter": {
"showDismissed": "무시된 항목 표시",
"severity": "심각도",
"error": "오류",
"warning": "경고",
@@ -605,6 +605,7 @@
}
},
"semanticSearch": {
"title": "Semantic Paieška išjungta",
"desc": "Norint naudoti Trigerius Semantic Paieška privalo būti įjungta."
}
},
@@ -615,6 +616,7 @@
"desc": "Frigate praneįimai sukurti veikti su push pranešimais į įrenginį kai naršoma per naršyklę arba įdiegta kaip PWA."
},
"notificationUnavailable": {
"title": "Pranešimai Negalimi",
"desc": "Web push pranešimai reikalauja saugios aplinkos (<code>https://...</code>). Tai yra naršyklės apribojimai. Atsidarykit Frigate saugiu kanalu kad galėtumėte naudotis pranešimais."
},
"globalSettings": {
+2
View File
@@ -227,6 +227,8 @@
},
"lastRefreshed": "Paskutinį kartą atnaujinta: ",
"stats": {
"ffmpegHighCpuUsage": "{{camera}} turi aukštą CPU suvartojimą FFmpeg ({{ffmpegAvg}}%)",
"detectHighCpuUsage": "{{camera}} turi auktšą CPU vartojimą aptikimams ({{detectAvg}}%)",
"healthy": "Sistemos būklė sveika",
"reindexingEmbeddings": "Įterpinių reideksavimas ({{processed}}% baigtas)",
"cameraIsOffline": "{{camera}} yra nepasiekiama",
@@ -574,6 +574,7 @@
"title": "Innstillinger for meldingsvarsler"
},
"notificationUnavailable": {
"title": "Meldingsvarsler utilgjengelig",
"desc": "Nettleser push-varsler krever et sikkert miljø (<code>https://…</code>). Dette er en nettleserbegrensning. Få tilgang til Frigate på en sikker måte for å bruke meldingsvarsler.",
"descPwa": "På iOS er web-pushvarsler kun tilgjengelig når Frigate er installert på hjemskjermen din. Åpne <strong>Del</strong>-menyen, velg <strong>Legg til på hjemskjermen</strong>, og åpne deretter Frigate fra det nye ikonet for å registrere denne enheten for varsler."
},
@@ -838,6 +839,7 @@
}
},
"semanticSearch": {
"title": "Semantisk søk er deaktivert",
"desc": "Semantisk søk må aktiveres for å bruke utløsere."
},
"wizard": {
@@ -254,6 +254,8 @@
"metrics": "Systemmålinger",
"lastRefreshed": "Sist oppdatert: ",
"stats": {
"ffmpegHighCpuUsage": "{{camera}} har høy CPU-belastning for FFmpeg ({{ffmpegAvg}}%)",
"detectHighCpuUsage": "{{camera}} har høy CPU-belastning for detektering ({{detectAvg}}%)",
"healthy": "Systemet fungerer som det skal",
"reindexingEmbeddings": "Reindeksering av vektorrepresentasjoner ({{processed}}% fullført)",
"cameraIsOffline": "{{camera}} er frakoblet",
+2 -16
View File
@@ -37,22 +37,8 @@
"submittedFrigatePlus": "Frame succesvol ingediend bij Frigate+"
},
"error": {
"submitFrigatePlusFailed": "Het is niet gelukt om een frame naar Frigate+ te sturen",
"playRecordingsFailed": "Kan opnames niet afspelen (fout {{code}}): {{message}}"
"submitFrigatePlusFailed": "Het is niet gelukt om een frame naar Frigate+ te sturen"
}
},
"cameraOff": "De camera staat uit",
"quality": {
"autoLow": "Afspelen in lage kwaliteit (beperkte bandbreedte)",
"autoLowCodec": "Afspelen in lage kwaliteit (Orgineel niet ondersteund voor deze browser)",
"autoLowSaveData": "Afspelen in lage kwaliteit (databesparing)",
"main": "Origineel",
"sub": "Laag",
"notSupportedBrowser": "Niet ondersteund door deze browser",
"label": "Kwaliteit",
"noAudio": "Geen audio",
"audioRate": "{{rate}} kHz audio",
"audioCodecRate": "{{codec}} {{rate}} kHz",
"noRecordings": "Geen opnames in dit tijdsframe"
}
"cameraOff": "De camera staat uit"
}
+14 -190
View File
@@ -1,11 +1,11 @@
{
"label": "CameraConfiguratie",
"name": {
"label": "Cameranaam",
"description": "Cameranaam is vereist"
"label": "Camera naam",
"description": "Camera naam is vereist"
},
"friendly_name": {
"description": "Cameranaam te gebruiken in de Frigate UI",
"description": "Camera naam te gebruiken in de Frigate UI",
"label": "Eenvoudige naam"
},
"enabled": {
@@ -29,7 +29,7 @@
},
"listen": {
"label": "Luistercategorieën",
"description": "Lijst met categorieën van geluidsgebeurtenissen die moeten worden gedetecteerd (zoals blaffen, band_alarm, praten en schreeuwen)."
"description": "Lijst van luistercategorie gebeurtenissen voor detectie (zoals: blaffen, band_alarm, praten, schreeuwen)."
},
"filters": {
"label": "Geluidsfilters",
@@ -40,16 +40,12 @@
}
},
"enabled_in_config": {
"label": "Oorspronkelijke audiostatus",
"label": "Originele audio-instelling",
"description": "Geeft aan of audiodetectie oorspronkelijk was geactiveerd in het statische configuratiebestand."
},
"num_threads": {
"label": "Detectiethreads",
"description": "Aantal threads voor audiodetectieverwerking."
},
"labelmap": {
"label": "Aanpassing van audiolabels",
"description": "Overschrijvingen of hertoewijzingen die aan de standaard audiolabelset worden toegevoegd."
}
},
"audio_transcription": {
@@ -77,10 +73,6 @@
"order": {
"label": "Positie",
"description": "Numerieke positie die de volgorde van de camera in de overzichtsweergave lay-out bepaalt."
},
"modes": {
"label": "Activiteitstypen",
"description": "Activiteitstypen waarbij camera’s in Birdseye worden weergegeven."
}
},
"detect": {
@@ -104,7 +96,7 @@
},
"min_initialized": {
"label": "Minimale initialisatieframes",
"description": "Aantal opeenvolgende detectieresultaten dat vereist is voordat een gevolgd object wordt aangemaakt. Verhoog deze waarde om valse initialisaties te verminderen. De standaardwaarde is fps gedeeld door 2."
"description": "Aantal opeenvolgende detectieresultaten dat vereist is voordat een gevolgd object wordt aangemaakt. Verhoog deze waarde om valse initialisaties te verminderen. De standaardwaarde is FPS gedeeld door 2."
},
"max_disappeared": {
"label": "Maximaal aantal verdwenen frames",
@@ -141,15 +133,10 @@
"annotation_offset": {
"label": "Annotatie-offset",
"description": "Milliseconden om detectieannotaties te verschuiven voor betere uitlijning van tijdlijn-detectiekaders met opnames; kan positief of negatief zijn."
},
"scene": {
"label": "Scène detecteren",
"description": "De omgeving waarop deze camera is gericht. Hiermee wordt bepaald welk van de geconfigureerde modellen op deze camera wordt uitgevoerd. Voor camera's die op 'alles' staan, wordt het model gebruikt waarvoor de scène op 'alles' is ingesteld."
}
},
"profiles": {
"label": "Profielen",
"description": "Benoemde configuratieprofielen met specifieke afwijkende instellingen die tijdens runtime kunnen worden geactiveerd."
"label": "Profielen"
},
"zones": {
"label": "Zones",
@@ -166,63 +153,8 @@
"label": "Zone filters",
"description": "Filters die op objecten binnen deze zone moeten worden toegepast. Worden gebruikt om het aantal valse positieven te verminderen of om te beperken welke objecten als aanwezig in de zone worden beschouwd.",
"min_area": {
"label": "Minimale oppervlakte van het object",
"description": "Minimaal vereist oppervlak van het begrenzingskader voor dit objecttype (in pixels of als percentage). Kan worden opgegeven als aantal pixels (geheel getal) of als percentage (kommagetal tussen 0,000001 en 0,99)."
},
"max_area": {
"label": "Maximaal objectoppervlak",
"description": "Maximaal oppervlak van het begrenzingskader voor dit objecttype (in pixels of als percentage). Kan worden opgegeven als aantal pixels (geheel getal) of als percentage (kommagetal tussen 0,000001 en 0,99)."
},
"min_ratio": {
"label": "Minimale beeldverhouding",
"description": "Minimale verhouding tussen breedte en hoogte waaraan het begrenzingskader moet voldoen."
},
"max_ratio": {
"label": "Maximale beeldverhouding",
"description": "Maximale verhouding tussen breedte en hoogte waaraan het begrenzingskader mag voldoen."
},
"threshold": {
"label": "Betrouwbaarheidsdrempel",
"description": "Gemiddelde betrouwbaarheidsdrempel die een detectie moet behalen om het object als correct gedetecteerd te beschouwen."
},
"min_score": {
"label": "Minimale betrouwbaarheid",
"description": "Minimale betrouwbaarheid die een detectie in één frame moet hebben om het object mee te tellen."
},
"mask": {
"label": "Filter masker",
"description": "Polygooncoördinaten die bepalen op welk deel van het beeld dit filter wordt toegepast."
},
"raw_mask": {
"label": "Onbewerkte maskering"
"label": "Minimale oppervlakte van het object"
}
},
"enabled_in_config": {
"label": "De oorspronkelijke status van de zone bijhouden."
},
"coordinates": {
"label": "Coördinaten",
"description": "Polygooncoördinaten die het gebied van de zone bepalen. Kan worden opgegeven als een kommagescheiden tekenreeks of als een lijst met coördinaten. Coördinaten kunnen relatief (0-1) of absoluut (verouderd) zijn."
},
"distances": {
"label": "Werkelijke afstanden",
"description": "Optionele werkelijke afstanden voor elke zijde van de vierhoekige zone, gebruikt voor snelheids- of afstandsberekeningen. Indien ingesteld, moeten precies 4 waarden worden opgegeven."
},
"inertia": {
"label": "Traagheidsframes",
"description": "Aantal opeenvolgende frames waarin een object in de zone moet worden gedetecteerd voordat het als aanwezig wordt beschouwd. Dit helpt kortstondige detecties uit te filteren."
},
"loitering_time": {
"label": "Minimale verblijftijd in seconden",
"description": "Aantal seconden dat een object in de zone moet blijven voordat dit als rondhangen wordt beschouwd. Stel in op 0 om detectie van rondhangen uit te schakelen."
},
"speed_threshold": {
"label": "Minimale snelheid",
"description": "Minimale snelheid die een object moet hebben om als aanwezig in de zone te worden beschouwd. Als afstanden zijn ingesteld, wordt de snelheid uitgedrukt in werkelijke eenheden. Wordt gebruikt voor zoneactivering op basis van snelheid."
},
"objects": {
"label": "Objecten voor activering",
"description": "Lijst met objecttypen (uit de labelmap) die deze zone kunnen activeren. Kan één tekenreeks of een lijst met tekenreeksen zijn. Indien leeg, worden alle objecten meegenomen."
}
},
"mqtt": {
@@ -237,31 +169,13 @@
"description": "Plaats een tijdstempel als overlay op afbeeldingen die naar MQTT worden gepubliceerd."
},
"bounding_box": {
"label": "Voeg een kader toe rond gedetecteerde objecten",
"description": "Teken begrenzingskaders op afbeeldingen die via MQTT worden gepubliceerd."
},
"crop": {
"label": "Afbeelding bijsnijden",
"description": "Naar MQTT gepubliceerde afbeeldingen bijsnijden tot het detectiekader van het gedetecteerde object."
},
"height": {
"label": "Afbeeldingshoogte",
"description": "Hoogte (pixels) voor het schalen van via MQTT gepubliceerde afbeeldingen."
},
"required_zones": {
"label": "Vereiste zones",
"description": "Zones dat een object moet betreden om een MQTT afbeelding te publiceren."
},
"quality": {
"label": "JPEG kwaliteit",
"description": "JPEG kwaliteit voor afbeeldingen gepubliceerd via MQTT (0-100)."
"label": "Voeg een kader toe rond gedetecteerde objecten"
}
},
"notifications": {
"label": "Meldingen",
"enabled": {
"label": "Meldingen inschakelen",
"description": "Notificaties voor deze camera in of uitschakelen."
"label": "Meldingen inschakelen"
},
"email": {
"label": "Melding email",
@@ -274,8 +188,7 @@
"enabled_in_config": {
"label": "Originele meldingsstatus",
"description": "Geeft aan of meldingen waren ingeschakeld in de originele statische configuratie."
},
"description": "Instelling om notificatie instellingen in te schakelen voor deze camera en te beheren."
}
},
"ffmpeg": {
"label": "Streams (FFmpeg)",
@@ -306,10 +219,6 @@
"record": {
"label": "Uitvoerargumenten voor opname",
"description": "Standaard uitvoerargumenten voor streams met opnamerol."
},
"record_sub": {
"label": "Uitvoerargumenten voor opname van de substream",
"description": "Uitvoerargumenten voor streams met de rol record_sub. Als dit niet is ingesteld, worden de uitvoerargumenten voor opname gebruikt."
}
},
"retry_interval": {
@@ -424,8 +333,7 @@
"label": "Objecten",
"description": "Standaardinstellingen voor objectvolging, inclusief te volgen labels en per-object filters.",
"track": {
"label": "Te volgen objecten",
"description": "Lijst met objectlabels om te volgen voor deze camera."
"label": "Te volgen objecten"
},
"filters": {
"label": "Objectfilters",
@@ -619,55 +527,7 @@
"label": "Originele opnamestatus",
"description": "Geeft aan of opname was ingeschakeld in de originele statische configuratie."
},
"description": "Instellingen opname en bewaarbeleid voor deze camera.",
"sub": {
"label": "Substream opname",
"description": "Instellingen voor het opnemen van een tweede stream met lagere kwaliteit.",
"enabled": {
"label": "Substream opname inschakelen",
"description": "Het opnemen van een tweede stream met lagere kwaliteit inschakelen voor adaptieve afspeelkwaliteit en langere bewaartermijn."
},
"continuous": {
"label": "Bewaartermijn voor continue substream opnames",
"description": "Aantal dagen dat substream opnames worden bewaard, ongeacht gedetecteerde objecten of beweging.",
"days": {
"label": "Bewaartermijn in dagen",
"description": "Aantal dagen dat opnames worden bewaard."
}
},
"motion": {
"label": "Bewaartermijn voor substream opnames met beweging",
"description": "Aantal dagen dat substream opnames met gedetecteerde beweging worden bewaard.",
"days": {
"label": "Bewaartermijn in dagen",
"description": "Dagen om opnames te bewaren."
}
},
"alerts": {
"label": "Bewaartermijn voor substream opnames met waarschuwingen",
"description": "Bewaarinstellingen voor substream opnames met waarschuwingen.",
"days": {
"label": "Bewaardagen",
"description": "Aantal dagen om opnames van detectiegebeurtenissen te bewaren."
},
"mode": {
"label": "Bewaarmodus",
"description": "Bewaarmodus: all (alle segmenten), motion (segmenten met beweging) of active_objects (segmenten met actieve objecten)."
}
},
"detections": {
"label": "Bewaartermijn voor substream opnames met detecties",
"description": "Bewaarinstellingen voor substream opnames met detecties.",
"days": {
"label": "Bewaardagen",
"description": "Aantal dagen om opnames van detectiegebeurtenissen te bewaren."
},
"mode": {
"label": "Bewaarmodus",
"description": "Bewaarmodus: all (alle segmenten), motion (segmenten met beweging) of active_objects (segmenten met actieve objecten)."
}
}
}
"description": "Instellingen opname en bewaarbeleid voor deze camera."
},
"review": {
"label": "Beoordeling",
@@ -757,14 +617,6 @@
"activity_context_prompt": {
"label": "Activiteitscontextprompt",
"description": "Aangepaste prompt die beschrijft wat wel en niet verdachte activiteit is, als context voor GenAI-samenvattingen."
},
"frame_mode": {
"label": "Framemodus",
"description": "Hoe frames aan het model worden aangeboden. 'frames' stuurt eerst de prompt en daarna de frames en is geschikt voor modellen die een reeks zelfstandig goed kunnen volgen. 'annotated_frames' voorziet elk frame van een label en voegt tussendoor notities uit objecttracking toe. Dit helpt modellen die moeite hebben om herhaalde of omgekeerde bewegingen goed te volgen."
},
"response_style": {
"label": "Antwoordstijl",
"description": "Voorinstelling voor de schrijfstijl van gegenereerde beoordelingsbeschrijvingen. Deze bepaalt de toon en het detailniveau van de titel, samenvatting en scènebeschrijving voor de gebruiker. Met default blijft de standaardprompt ongewijzigd."
}
},
"description": "Instellingen voor waarschuwingen, detecties en GenAI‑samenvattingen die worden gebruikt door de interface en de opslag van deze camera."
@@ -991,34 +843,6 @@
},
"best_image_timeout": {
"label": "Timeout voor het ophalen van de beste afbeelding",
"description": "Hoe lang er gewacht wordt op de afbeelding met de hoogste score van betrouwbaarheid."
},
"type": {
"label": "Cameratype",
"description": "Cameratype"
},
"ui": {
"label": "Camera interface",
"description": "Weergavevolgorde en zichtbaarheid van deze camera in de gebruikersinterface. De volgorde bepaalt de standaardweergave op het dashboard. Gebruik cameragroepen voor meer gedetailleerde instellingen.",
"order": {
"label": "Volgorde in de interface",
"description": "Numerieke volgorde waarmee de camera in de interface wordt gesorteerd (standaarddashboard en lijsten). Hogere waarden worden later weergegeven."
},
"dashboard": {
"label": "Weergeven op het Live dashboard",
"description": "Bepaal of deze camera zichtbaar is op het standaard Live dashboard met alle camera's. De camera blijft elders in de interface beschikbaar, zoals in cameragroepen en instellingen."
},
"review": {
"label": "Weergeven in beoordelingen",
"description": "Bepaal of deze camera wordt weergegeven in beoordelingen (waaronder de beoordelingspagina, in het camerafilter, bij bewegingsdetecties en in de geschiedenisweergave)."
}
},
"webui_url": {
"label": "Camera URL",
"description": "URL om de camera rechtstreeks vanuit de systeempagina te openen"
},
"enabled_in_config": {
"label": "Oorspronkelijke camerastatus",
"description": "De oorspronkelijke status van de camera bijhouden."
"description": "Hoe lang er gewacht wordt op de afbeelding met de hoogste score van betrouwbaarheid"
}
}
+4 -76
View File
@@ -15,7 +15,7 @@
},
"listen": {
"label": "Luistercategorieën",
"description": "Lijst met categorieën van geluidsgebeurtenissen die moeten worden gedetecteerd (zoals blaffen, band_alarm, praten en schreeuwen)."
"description": "Lijst van luistercategorie gebeurtenissen voor detectie (zoals: blaffen, band_alarm, praten, schreeuwen)."
},
"filters": {
"label": "Geluidsfilters",
@@ -26,18 +26,14 @@
}
},
"enabled_in_config": {
"label": "Oorspronkelijke audiostatus",
"label": "Originele audio-instelling",
"description": "Geeft aan of audiodetectie oorspronkelijk was geactiveerd in het statische configuratiebestand."
},
"num_threads": {
"label": "Detectiethreads",
"description": "Aantal threads voor audiodetectieverwerking."
},
"description": "Instellingen voor audiogebaseerde gebeurtenisdetectie voor alle camera's; kan per camera worden overschreven.",
"labelmap": {
"label": "Aanpassing van audiolabels",
"description": "Overschrijvingen of hertoewijzingen die aan de standaard audiolabelset worden toegevoegd."
}
"description": "Instellingen voor audiogebaseerde gebeurtenisdetectie voor alle camera's; kan per camera worden overschreven."
},
"audio_transcription": {
"label": "Audiotranscriptie",
@@ -109,10 +105,6 @@
"idle_heartbeat_fps": {
"label": "Inactief heartbeat-FPS",
"description": "Frames per seconde voor het opnieuw verzenden van het laatste Birdseye-frame tijdens inactiviteit; stel 0 in om uit te schakelen."
},
"modes": {
"label": "Activiteitstypen",
"description": "Activiteitstypen waarbij camera’s in Birdseye worden weergegeven."
}
},
"detect": {
@@ -136,7 +128,7 @@
},
"min_initialized": {
"label": "Minimale initialisatieframes",
"description": "Aantal opeenvolgende detectieresultaten dat vereist is voordat een gevolgd object wordt aangemaakt. Verhoog deze waarde om valse initialisaties te verminderen. De standaardwaarde is fps gedeeld door 2."
"description": "Aantal opeenvolgende detectieresultaten dat vereist is voordat een gevolgd object wordt aangemaakt. Verhoog deze waarde om valse initialisaties te verminderen. De standaardwaarde is FPS gedeeld door 2."
},
"max_disappeared": {
"label": "Maximaal aantal verdwenen frames",
@@ -173,10 +165,6 @@
"annotation_offset": {
"label": "Annotatie-offset",
"description": "Milliseconden om detectieannotaties te verschuiven voor betere uitlijning van tijdlijn-detectiekaders met opnames; kan positief of negatief zijn."
},
"scene": {
"label": "Scène detecteren",
"description": "De omgeving waarop deze camera is gericht. Hiermee wordt bepaald welk van de geconfigureerde modellen op deze camera wordt uitgevoerd. Voor camera's die op 'alles' staan, wordt het model gebruikt waarvoor de scène op 'alles' is ingesteld."
}
},
"version": {
@@ -524,10 +512,6 @@
"record": {
"label": "Uitvoerargumenten voor opname",
"description": "Standaard uitvoerargumenten voor streams met opnamerol."
},
"record_sub": {
"label": "Uitvoerargumenten voor opname van de substream",
"description": "Uitvoerargumenten voor streams met de rol record_sub. Als dit niet is ingesteld, worden de uitvoerargumenten voor opname gebruikt."
}
},
"retry_interval": {
@@ -837,54 +821,6 @@
"enabled_in_config": {
"label": "Originele opnamestatus",
"description": "Geeft aan of opname was ingeschakeld in de originele statische configuratie."
},
"sub": {
"label": "Substream opname",
"description": "Instellingen voor het opnemen van een tweede stream met lagere kwaliteit.",
"enabled": {
"label": "Substream opname inschakelen",
"description": "Het opnemen van een tweede stream met lagere kwaliteit inschakelen voor adaptieve afspeelkwaliteit en langere bewaartermijn."
},
"continuous": {
"label": "Bewaartermijn voor continue substream opnames",
"description": "Aantal dagen dat substream opnames worden bewaard, ongeacht gedetecteerde objecten of beweging.",
"days": {
"label": "Bewaartermijn in dagen",
"description": "Aantal dagen dat opnames worden bewaard."
}
},
"motion": {
"label": "Bewaartermijn voor substream opnames met beweging",
"description": "Aantal dagen dat substream opnames met gedetecteerde beweging worden bewaard.",
"days": {
"label": "Bewaartermijn in dagen",
"description": "Dagen om opnames te bewaren."
}
},
"alerts": {
"label": "Bewaartermijn voor substream opnames met waarschuwingen",
"description": "Bewaarinstellingen voor substream opnames met waarschuwingen.",
"days": {
"label": "Bewaardagen",
"description": "Aantal dagen om opnames van detectiegebeurtenissen te bewaren."
},
"mode": {
"label": "Bewaarmodus",
"description": "Bewaarmodus: all (alle segmenten), motion (segmenten met beweging) of active_objects (segmenten met actieve objecten)."
}
},
"detections": {
"label": "Bewaartermijn voor substream opnames met detecties",
"description": "Bewaarinstellingen voor substream opnames met detecties.",
"days": {
"label": "Bewaardagen",
"description": "Aantal dagen om opnames van detectiegebeurtenissen te bewaren."
},
"mode": {
"label": "Bewaarmodus",
"description": "Bewaarmodus: all (alle segmenten), motion (segmenten met beweging) of active_objects (segmenten met actieve objecten)."
}
}
}
},
"review": {
@@ -976,14 +912,6 @@
"activity_context_prompt": {
"label": "Activiteitscontextprompt",
"description": "Aangepaste prompt die beschrijft wat wel en niet verdachte activiteit is, als context voor GenAI-samenvattingen."
},
"frame_mode": {
"label": "Framemodus",
"description": "Hoe frames aan het model worden aangeboden. 'frames' stuurt eerst de prompt en daarna de frames en is geschikt voor modellen die een reeks zelfstandig goed kunnen volgen. 'annotated_frames' voorziet elk frame van een label en voegt tussendoor notities uit objecttracking toe. Dit helpt modellen die moeite hebben om herhaalde of omgekeerde bewegingen goed te volgen."
},
"response_style": {
"label": "Antwoordstijl",
"description": "Voorinstelling voor de schrijfstijl van gegenereerde beoordelingsbeschrijvingen. Deze bepaalt de toon en het detailniveau van de titel, samenvatting en scènebeschrijving voor de gebruiker. Met default blijft de standaardprompt ongewijzigd."
}
}
},
@@ -574,6 +574,7 @@
"desc": "Frigate kan rechtstreeks pushmeldingen naar uw apparaat verzenden als het in de browser actief is of als een PWA geïnstalleerd is."
},
"notificationUnavailable": {
"title": "Meldingen niet beschikbaar",
"desc": "Webpushmeldingen vereisen een veilige omgeving (<code>https://…</code>). Dit is een beperking van de browser. Open Frigate via een beveiligde verbinding om meldingen te kunnen ontvangen."
},
"globalSettings": {
@@ -837,6 +838,7 @@
}
},
"semanticSearch": {
"title": "Semantisch zoeken is uitgeschakeld",
"desc": "Semantisch zoeken moet ingeschakeld zijn om triggers te kunnen gebruiken."
},
"wizard": {
+2
View File
@@ -189,6 +189,8 @@
},
"lastRefreshed": "Voor het laatst vernieuwd: ",
"stats": {
"ffmpegHighCpuUsage": "{{camera}} zorgt voor hoge FFmpeg CPU belasting ({{ffmpegAvg}}%)",
"detectHighCpuUsage": "{{camera}} zorgt voor hoge detectie CPU belasting ({{detectAvg}}%)",
"healthy": "Geen problemen",
"reindexingEmbeddings": "Herindexering van inbeddingen ({{processed}}% compleet)",
"detectIsSlow": "{{detect}} is traag ({{speed}} ms)",
@@ -578,6 +578,7 @@
"desc": "Frigate może wysyłać natywne powiadomienia push na twoje urządzenie, gdy działa w przeglądarce lub jest zainstalowany jako PWA."
},
"notificationUnavailable": {
"title": "Powiadomienia niedostępne",
"desc": "Powiadomienia push w przeglądarce wymagają bezpiecznego kontekstu (<code>https://…</code>). To jest ograniczenie przeglądarki. Uzyskaj dostęp do Frigate przez bezpieczne połączenie, aby korzystać z powiadomień.",
"descPwa": "W systemie iOS powiadomienia web push są dostępne tylko wtedy, gdy Frigate jest zainstalowane na ekranie głównym. Otwórz menu <strong>Udostępnij</strong>, wybierz <strong>Do ekranu początkowego</strong>, a następnie otwórz Frigate z nowej ikony, aby zarejestrować to urządzenie do powiadomień."
},
@@ -903,6 +904,7 @@
}
},
"semanticSearch": {
"title": "Wyszukiwanie semantyczne jest zablokowane",
"desc": "Wyszukiwanie semantyczne musi być włączone, aby korzystać z triggerów."
},
"wizard": {
+2
View File
@@ -227,6 +227,8 @@
"metrics": "Metryki systemowe",
"lastRefreshed": "Ostatnie odświeżenie: ",
"stats": {
"ffmpegHighCpuUsage": "{{camera}} ma wysokie użycie CPU przez FFmpeg ({{ffmpegAvg}}%)",
"detectHighCpuUsage": "{{camera}} ma wysokie użycie CPU przez detekcję ({{detectAvg}}%)",
"healthy": "System jest sprawny",
"reindexingEmbeddings": "Ponowne indeksowanie osadzeń ({{processed}}% ukończone)",
"detectIsSlow": "{{detect}} jest wolne ({{speed}} ms)",
@@ -664,10 +664,6 @@
"response_style": {
"label": "Estilo de resposta",
"description": "Estilo de escrita pré-definido para revisão de descrições geradas. A predefinição ajusta o tom e nível de detalhes do título, resumo e descrição das cenas; 'default' deixa o prompt interno sem alterações."
},
"frame_mode": {
"label": "Modo de quadros",
"description": "Como os quadros são apresentados ao modelo. 'frames' envia o prompt seguido dos quadros, o que serve para modelos que acompanham bem uma sequência por conta própria. 'annotated_frames' rotula cada quadro e intercala notas derivadas do rastreamento de objetos, o que ajuda modelos que perdem o fio de atividades que se repetem ou se invertem."
}
}
},
+23 -650
View File
@@ -56,11 +56,6 @@
"labelmap": {
"label": "Personalização de etiquetas de áudio",
"description": "Sobrepõe ou re-mapeia entradas para mesclar ao lablemap de áudio padrão."
},
"description": "Configurações da detecção de eventos baseada em áudio para todas as câmeras; podem ser substituídas por câmera.",
"enabled": {
"label": "Habilitar detecção de áudio",
"description": "Habilita ou desabilita a detecção de eventos de áudio em todas as câmeras; pode ser substituído por câmera."
}
},
"auth": {
@@ -93,22 +88,6 @@
"refresh_time": {
"label": "Janela de atualização da sessão",
"description": "Quando uma seção está a poucos segundos da expiração, atualizar para o tempo total."
},
"trusted_proxies": {
"label": "Proxies confiáveis",
"description": "Lista de IPs de proxies confiáveis usados para determinar o IP do cliente na limitação de taxa."
},
"hash_iterations": {
"label": "Iterações de hash",
"description": "Número de iterações PBKDF2-SHA256 a serem usadas ao gerar o hash das senhas dos usuários."
},
"roles": {
"label": "Mapeamentos de funções",
"description": "Mapeia funções para listas de câmeras. Uma lista vazia concede acesso a todas as câmeras para a função."
},
"admin_first_time_login": {
"label": "Sinalizador de primeiro acesso do administrador",
"description": "Quando verdadeiro, a interface pode exibir na página de login um link de ajuda informando aos usuários como entrar após a redefinição da senha do administrador. "
}
},
"audio_transcription": {
@@ -121,28 +100,11 @@
"enabled": {
"label": "Habilitar a transcrição de áudio",
"description": "Ativar ou desativar a transcrição automática de áudio para todas as câmeras; pode ser configurado individualmente por câmera."
},
"language": {
"label": "Idioma da transcrição",
"description": "Código de idioma usado para transcrição/tradução (por exemplo, 'en' para inglês) ou 'auto' para que o modelo o detecte. Consulte https://whisper-api.com/docs/languages/ para ver os códigos de idioma compatíveis."
},
"device": {
"label": "Dispositivo de transcrição",
"description": "Chave do dispositivo (CPU/GPU) em que o modelo de transcrição será executado. Atualmente, apenas GPUs NVIDIA com CUDA são compatíveis com a transcrição."
},
"model_size": {
"label": "Tamanho do modelo",
"description": "Tamanho do modelo a ser usado na transcrição offline de eventos de áudio."
},
"model": {
"label": "Modelo de transcrição de áudio ou nome do provedor de IA generativa",
"description": "O backend de transcrição: 'whisper' para os modelos locais integrados do Frigate, ou o nome de um provedor de IA generativa com a função transcribe."
}
},
"detect": {
"enabled": {
"label": "Ativar detecção de objetos",
"description": "Habilita ou desabilita a detecção de objetos para todas as câmeras; pode ser substituído por câmera."
"label": "Ativar detecção de objetos"
},
"height": {
"label": "Detectar altura",
@@ -206,45 +168,11 @@
"face_recognition": {
"label": "Reconhecimento facial",
"enabled": {
"label": "Ativar reconhecimento facial",
"description": "Habilita ou desabilita o reconhecimento facial para todas as câmeras; pode ser substituído por câmera."
"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."
},
"description": "Configurações de detecção e reconhecimento facial para todas as câmeras; podem ser substituídas por câmera.",
"model_size": {
"label": "Tamanho do modelo",
"description": "Tamanho do modelo a ser usado nos embeddings faciais (small/large); o maior pode exigir GPU."
},
"unknown_score": {
"description": "Limite de distância abaixo do qual um rosto é considerado uma possível correspondência (quanto maior, mais rigoroso).",
"label": "Limite de pontuação para desconhecido"
},
"detection_threshold": {
"label": "Limite de detecção",
"description": "Confiança mínima de detecção necessária para considerar válida uma detecção de rosto."
},
"recognition_threshold": {
"label": "Limite de reconhecimento",
"description": "Limite de distância entre embeddings faciais para considerar que dois rostos correspondem."
},
"min_faces": {
"label": "Mínimo de rostos",
"description": "Número mínimo de reconhecimentos faciais necessários antes de aplicar um sub-rótulo reconhecido a uma pessoa."
},
"save_attempts": {
"label": "Tentativas a salvar",
"description": "Número de tentativas de reconhecimento facial a manter para a interface de reconhecimentos recentes."
},
"blur_confidence_filter": {
"description": "Ajusta as pontuações de confiança com base no desfoque da imagem para reduzir falsos positivos em rostos de baixa qualidade.",
"label": "Filtro de confiança por desfoque"
},
"device": {
"label": "Dispositivo",
"description": "Esta é uma substituição, para escolher um dispositivo específico. Consulte https://onnxruntime.ai/docs/execution-providers/ para mais informações"
}
},
"ffmpeg": {
@@ -315,9 +243,7 @@
"label": "Argumentos de entrada",
"description": "Argumentos de entrada específicos para este fluxo."
}
},
"label": "FFmpeg",
"description": "Configurações do FFmpeg, incluindo caminho do binário, argumentos, opções de hwaccel e argumentos de saída por função."
}
},
"birdseye": {
"order": {
@@ -333,42 +259,6 @@
"modes": {
"label": "Tipos de Atividade",
"description": "Tipos de atividade que incluem câmeras em modo panorâmico."
},
"restream": {
"label": "Restream RTSP",
"description": "Retransmite a saída do Birdseye como um feed RTSP; habilitar isto manterá o Birdseye em execução contínua."
},
"width": {
"description": "Largura de saída (em pixels) do quadro composto do Birdseye.",
"label": "Largura"
},
"height": {
"label": "Altura",
"description": "Altura de saída (em pixels) do quadro composto do Birdseye."
},
"quality": {
"label": "Qualidade de codificação",
"description": "Qualidade de codificação do feed mpeg1 do Birdseye (1 é a maior qualidade, 31 a menor)."
},
"inactivity_threshold": {
"label": "Limite de inatividade",
"description": "Segundos de inatividade após os quais uma câmera deixará de ser exibida no Birdseye."
},
"layout": {
"label": "Layout",
"description": "Opções de layout para a composição do Birdseye.",
"scaling_factor": {
"label": "Fator de escala",
"description": "Fator de escala usado pelo cálculo de layout (intervalo de 1,0 a 5,0)."
},
"max_cameras": {
"description": "Número máximo de câmeras exibidas ao mesmo tempo no Birdseye; mostra as câmeras mais recentes.",
"label": "Máximo de câmeras"
}
},
"idle_heartbeat_fps": {
"label": "FPS do heartbeat em inatividade",
"description": "Quadros por segundo com os quais o último quadro composto do Birdseye é reenviado quando ocioso; defina como 0 para desabilitar."
}
},
"live": {
@@ -384,13 +274,11 @@
"quality": {
"description": "Qualidade de codificação para o fluxo jsmpeg (1 para a mais alta, 31 para a mais baixa).",
"label": "Qualidade da transmissão"
},
"description": "Configurações para controlar a resolução e a qualidade do stream ao vivo jsmpeg. Isto não afeta as câmeras com restream que usam o go2rtc para a visualização ao vivo."
}
},
"motion": {
"enabled": {
"label": "Ativar detecção de movimento",
"description": "Habilita ou desabilita a detecção de movimento para todas as câmeras; pode ser substituído por câmera."
"label": "Ativar detecção de movimento"
},
"threshold": {
"label": "Limiar de movimento",
@@ -439,15 +327,13 @@
"raw_mask": {
"label": "Máscara Bruta"
},
"label": "Detecção de movimento",
"description": "Configurações padrão de detecção de movimento aplicadas às câmeras, a menos que sejam substituídas por câmera."
"label": "Detecção de movimento"
},
"objects": {
"label": "Objetos",
"description": "Configurações padrão de rastreamento de objetos, incluindo quais rótulos rastrear e filtros por objeto.",
"track": {
"label": "Objetos a rastrear",
"description": "Lista de rótulos de objetos a serem rastreados em todas as câmeras; pode ser substituída por câmera."
"label": "Objetos a rastrear"
},
"filters": {
"label": "Filtros de objeto",
@@ -538,47 +424,11 @@
"label": "Estado original da GenAI",
"description": "Indica se a GenAI foi habilitada na configuração estática original."
}
},
"filters_attribute": {
"label": "Filtros de atributos",
"description": "Filtros aplicados aos atributos detectados para reduzir falsos positivos (área, proporção, confiança).",
"min_area": {
"label": "Área mínima do atributo",
"description": "Área mínima da caixa delimitadora (pixels ou porcentagem) exigida para este atributo. Pode ser em pixels (inteiro) ou porcentagem (decimal entre 0,000001 e 0,99)."
},
"max_area": {
"label": "Área máxima do atributo",
"description": "Área máxima da caixa delimitadora (pixels ou porcentagem) permitida para este atributo. Pode ser em pixels (inteiro) ou porcentagem (decimal entre 0,000001 e 0,99)."
},
"min_ratio": {
"description": "Proporção mínima entre largura e altura exigida para que a caixa delimitadora seja considerada.",
"label": "Proporção mínima"
},
"max_ratio": {
"label": "Proporção máxima",
"description": "Proporção máxima entre largura e altura permitida para que a caixa delimitadora seja considerada."
},
"threshold": {
"label": "Limite de confiança",
"description": "Limite de confiança média de detecção necessário para que o atributo seja considerado um verdadeiro positivo."
},
"min_score": {
"label": "Confiança mínima",
"description": "Confiança mínima de detecção em um único quadro necessária para associar este atributo ao seu objeto pai."
},
"mask": {
"label": "Máscara do filtro",
"description": "Coordenadas do polígono que definem onde este filtro se aplica dentro do quadro."
},
"raw_mask": {
"label": "Máscara bruta"
}
}
},
"lpr": {
"enabled": {
"label": "Ativar LPR",
"description": "Habilita ou desabilita o reconhecimento de placas de veículos para todas as câmeras; pode ser substituído por câmera."
"label": "Ativar LPR"
},
"expire_time": {
"label": "Segundos para expirar",
@@ -593,59 +443,12 @@
"description": "Nível de aprimoramento (0-10) a ser aplicado aos recortes da placa antes do OCR; valores mais altos nem sempre melhoram os resultados, e níveis acima de 5 podem funcionar apenas com placas noturnas, devendo ser utilizados com cautela."
},
"label": "Reconhecimento de placa veicular",
"description": "Configurações de reconhecimento de placa veicular, incluindo limites de detecção, formatação e placas conhecidas.",
"model_size": {
"label": "Tamanho do modelo",
"description": "Tamanho do modelo usado para detecção/reconhecimento de texto. A maioria dos usuários deve usar 'small'."
},
"detection_threshold": {
"description": "Limite de confiança de detecção para iniciar o OCR em uma suspeita de placa.",
"label": "Limite de detecção"
},
"recognition_threshold": {
"label": "Limite de reconhecimento",
"description": "Limite de confiança necessário para que o texto da placa reconhecida seja anexado como sub-rótulo."
},
"min_plate_length": {
"label": "Tamanho mínimo da placa",
"description": "Número mínimo de caracteres que uma placa reconhecida deve conter para ser considerada válida."
},
"format": {
"label": "Regex do formato da placa",
"description": "Regex opcional para validar as placas reconhecidas em relação a um formato esperado."
},
"match_distance": {
"label": "Distância de correspondência",
"description": "Número de caracteres diferentes permitidos ao comparar as placas detectadas com as placas conhecidas."
},
"known_plates": {
"description": "Lista de placas ou regexes a serem rastreadas ou alertadas de forma especial.",
"label": "Placas conhecidas"
},
"debug_save_plates": {
"label": "Salvar placas para depuração",
"description": "Salva as imagens recortadas das placas para depurar o desempenho do LPR."
},
"device": {
"label": "Dispositivo",
"description": "Esta é uma substituição, para escolher um dispositivo específico. Consulte https://onnxruntime.ai/docs/execution-providers/ para mais informações"
},
"replace_rules": {
"label": "Regras de substituição",
"description": "Regras de substituição por regex usadas para normalizar as placas detectadas antes da comparação.",
"pattern": {
"label": "Padrão regex"
},
"replacement": {
"label": "Texto de substituição"
}
}
"description": "Configurações de reconhecimento de placa veicular, incluindo limites de detecção, formatação e placas conhecidas."
},
"record": {
"label": "Gravação",
"enabled": {
"label": "Ativar gravação",
"description": "Habilita ou desabilita a gravação para todas as câmeras; pode ser substituído por câmera."
"label": "Ativar gravação"
},
"expire_interval": {
"label": "Intervalo de limpeza de gravação",
@@ -789,8 +592,7 @@
"description": "Modo de retenção: all (salvar todos os segmentos), motion (salvar segmentos com movimento), ou active_objects (salvar segmentos com objetos ativos)."
}
}
},
"description": "Configurações de gravação e retenção aplicadas às câmeras, a menos que sejam substituídas por câmera."
}
},
"review": {
"label": "Rever",
@@ -798,8 +600,7 @@
"label": "Configuração de alertas",
"description": "Configurações sobre quais objetos monitorados geram alertas e como os alertas são retidos.",
"enabled": {
"label": "Ativar alertas",
"description": "Habilita ou desabilita a geração de alertas para todas as câmeras; pode ser substituído por câmera."
"label": "Ativar alertas"
},
"labels": {
"label": "Etiquetas de alerta",
@@ -822,8 +623,7 @@
"label": "Configuração de detecções",
"description": "Configurações que definem para quais objetos rastreados são geradas detecções (sem gerar alerta) e como essas detecções são retidas.",
"enabled": {
"label": "Ativar detecções",
"description": "Habilita ou desabilita os eventos de detecção para todas as câmeras; pode ser substituído por câmera."
"label": "Ativar detecções"
},
"labels": {
"label": "Rótulos de detecção",
@@ -884,13 +684,8 @@
"response_style": {
"label": "Estilo de resposta",
"description": "Estilo de escrita pré-definido para revisão de descrições geradas. A predefinição ajusta o tom e nível de detalhes do título, resumo e descrição das cenas; 'default' deixa o prompt interno sem alterações."
},
"frame_mode": {
"label": "Modo de quadros",
"description": "Como os quadros são apresentados ao modelo. 'frames' envia o prompt seguido dos quadros, o que serve para modelos que acompanham bem uma sequência por conta própria. 'annotated_frames' rotula cada quadro e intercala notas derivadas do rastreamento de objetos, o que ajuda modelos que perdem o fio de atividades que se repetem ou se invertem."
}
},
"description": "Configurações que controlam alertas, detecções e resumos de revisão por IA generativa usados pela interface e pelo armazenamento."
}
},
"semantic_search": {
"label": "Busca semântica",
@@ -921,34 +716,12 @@
"label": "Acionar ações",
"description": "Lista de ações a serem executadas quando o gatilho for acionado (notificação, sub-rótulo, atributo)."
}
},
"description": "Configurações da Busca semântica, que cria e consulta embeddings de objetos para encontrar itens semelhantes.",
"enabled": {
"label": "Habilitar busca semântica",
"description": "Habilita ou desabilita o recurso de busca semântica."
},
"reindex": {
"description": "Aciona uma reindexação completa dos objetos rastreados históricos no banco de dados de embeddings.",
"label": "Reindexar na inicialização"
},
"model": {
"description": "O modelo de embeddings a ser usado na busca semântica (por exemplo, 'jinav1') ou o nome de um provedor de IA generativa com a função embeddings.",
"label": "Modelo de busca semântica ou nome do provedor de IA generativa"
},
"model_size": {
"label": "Tamanho do modelo",
"description": "Selecione o tamanho do modelo; 'small' é executado na CPU e 'large' normalmente exige GPU."
},
"device": {
"label": "Dispositivo",
"description": "Esta é uma substituição, para escolher um dispositivo específico. Consulte https://onnxruntime.ai/docs/execution-providers/ para mais informações"
}
},
"snapshots": {
"label": "Instantâneas",
"enabled": {
"label": "Habilitar instantâneas",
"description": "Habilita ou desabilita o salvamento de snapshots para todas as câmeras; pode ser substituído por câmera."
"label": "Habilitar instantâneas"
},
"timestamp": {
"label": "Sobreposição de marca de tempo",
@@ -985,13 +758,11 @@
"quality": {
"label": "Qualidade da captura",
"description": "Qualidade de codificação das capturas salvas (0-100)."
},
"description": "Configurações dos snapshots de objetos rastreados gerados pela API para todas as câmeras; podem ser substituídas por câmera."
}
},
"notifications": {
"enabled": {
"label": "Ativar notificações",
"description": "Habilita ou desabilita as notificações de todas as câmeras; pode ser substituído por câmera."
"label": "Ativar notificações"
},
"email": {
"label": "Notificação por E-mail",
@@ -1005,8 +776,7 @@
"cooldown": {
"label": "Intervalo de espera",
"description": "Intervalo (em segundos) entre notificações para evitar o envio excessivo de mensagens aos destinatários."
},
"description": "Configurações para habilitar e controlar as notificações de todas as câmeras; podem ser substituídas por câmera."
}
},
"onvif": {
"label": "ONVIF",
@@ -1117,415 +887,18 @@
"effect": {
"label": "Efeito da marcação de tempo",
"description": "Efeito visual para o texto do marcação de tempo (nenhum, sólido, sombra)."
},
"description": "Opções de estilo dos carimbos de data/hora exibidos no feed, aplicadas à visualização de depuração e aos snapshots."
}
},
"mqtt": {
"label": "MQTT",
"description": "Configurações para conectar e publicar telemetria, snapshots e detalhes de eventos em um broker MQTT.",
"enabled": {
"label": "Habilitar MQTT",
"description": "Habilita ou desabilita a integração MQTT para estados, eventos e snapshots."
},
"host": {
"label": "Host MQTT",
"description": "Nome de host ou endereço IP do broker MQTT."
},
"port": {
"label": "Porta MQTT",
"description": "Porta do broker MQTT (geralmente 1883 para MQTT sem criptografia)."
},
"topic_prefix": {
"description": "Prefixo de tópico MQTT para todos os tópicos do Frigate; deve ser único se você executar várias instâncias.",
"label": "Prefixo do tópico"
},
"client_id": {
"label": "ID do cliente",
"description": "Identificador do cliente usado ao se conectar ao broker MQTT; deve ser único para cada instância."
},
"stats_interval": {
"label": "Intervalo de estatísticas",
"description": "Intervalo, em segundos, para publicar estatísticas do sistema e das câmeras no MQTT."
},
"user": {
"label": "Usuário MQTT",
"description": "Usuário MQTT opcional; pode ser fornecido por variáveis de ambiente ou segredos."
},
"password": {
"label": "Senha MQTT",
"description": "Senha MQTT opcional; pode ser fornecida por variáveis de ambiente ou segredos."
},
"tls_ca_certs": {
"description": "Caminho para o certificado da CA para conexões TLS com o broker (para certificados autoassinados).",
"label": "Certificados CA do TLS"
},
"tls_client_cert": {
"label": "Certificado do cliente",
"description": "Caminho do certificado do cliente para autenticação mútua TLS; não defina usuário/senha ao usar certificados de cliente."
},
"tls_client_key": {
"label": "Chave do cliente",
"description": "Caminho da chave privada do certificado do cliente."
},
"tls_insecure": {
"label": "TLS inseguro",
"description": "Permite conexões TLS inseguras ignorando a verificação do nome do host (não recomendado)."
},
"qos": {
"label": "QoS do MQTT",
"description": "Nível de Qualidade de Serviço para publicações/assinaturas MQTT (0, 1 ou 2)."
}
"label": "MQTT"
},
"profiles": {
"label": "Perfis",
"description": "Definições de perfis nomeados com nomes amigáveis. Os perfis das câmeras devem referenciar os nomes definidos aqui.",
"friendly_name": {
"label": "Nome amigável",
"description": "Nome de exibição deste perfil mostrado na interface."
}
"label": "Perfis"
},
"camera_ui": {
"label": "Interface da Câmera",
"description": "Ordem de exibição e visibilidade desta câmera na interface. A ordem afeta o painel padrão. Para um controle mais detalhado, use grupos de câmeras.",
"order": {
"label": "Ordem na interface",
"description": "Ordem numérica usada para ordenar a câmera na interface (painel padrão e listas); números maiores aparecem depois."
},
"dashboard": {
"label": "Mostrar no painel ao vivo",
"description": "Alterna se esta câmera fica visível no painel ao vivo padrão de Todas as câmeras. A câmera continua disponível em todos os outros lugares da interface, incluindo grupos de câmeras e configurações."
},
"review": {
"label": "Mostrar na revisão",
"description": "Alterna se esta câmera fica visível na revisão (a página de revisão e seu filtro de câmeras, a revisão de movimento e a visualização de histórico)."
}
"label": "Interface da Câmera"
},
"ui": {
"label": "UI",
"description": "Preferências da interface do usuário, como fuso horário, formatação de hora/data e unidades.",
"timezone": {
"description": "Fuso horário opcional a ser exibido em toda a interface (usa o horário local do navegador se não for definido).",
"label": "Fuso horário"
},
"time_format": {
"label": "Formato de hora",
"description": "Formato de hora a ser usado na interface (browser, 12hour ou 24hour)."
},
"unit_system": {
"label": "Sistema de unidades",
"description": "Sistema de unidades para exibição (metric ou imperial), usado na interface e no MQTT."
}
},
"database": {
"description": "Configurações do banco de dados SQLite usado pelo Frigate para armazenar os metadados de objetos rastreados e de gravações.",
"path": {
"label": "Caminho do banco de dados",
"description": "Caminho no sistema de arquivos onde o arquivo do banco de dados SQLite do Frigate será armazenado."
},
"label": "Banco de dados"
},
"go2rtc": {
"label": "go2rtc",
"description": "Configurações do serviço de restreaming go2rtc integrado, usado para retransmissão e tradução de streams ao vivo."
},
"networking": {
"label": "Rede",
"description": "Configurações relacionadas à rede, como a habilitação de IPv6 nos endpoints do Frigate.",
"ipv6": {
"label": "Configuração de IPv6",
"description": "Configurações específicas de IPv6 para os serviços de rede do Frigate.",
"enabled": {
"description": "Habilita o suporte a IPv6 nos serviços do Frigate (API e interface), quando aplicável.",
"label": "Habilitar IPv6"
}
},
"listen": {
"description": "Configuração das portas de escuta internas e externas. Destina-se a usuários avançados. Na maioria dos casos, recomenda-se alterar a seção de portas do seu arquivo Docker Compose.",
"internal": {
"label": "Porta interna",
"description": "Porta de escuta interna do Frigate (padrão 5000)."
},
"label": "Configuração das portas de escuta",
"external": {
"label": "Porta externa",
"description": "Porta de escuta externa do Frigate (padrão 8971)."
}
}
},
"proxy": {
"label": "Proxy",
"header_map": {
"label": "Mapeamento de cabeçalhos",
"description": "Mapeia os cabeçalhos recebidos do proxy para os campos de usuário e função do Frigate na autenticação via proxy.",
"user": {
"description": "Cabeçalho que contém o nome de usuário autenticado fornecido pelo proxy de origem.",
"label": "Cabeçalho de usuário"
},
"role": {
"label": "Cabeçalho de função",
"description": "Cabeçalho que contém a função ou os grupos do usuário autenticado fornecidos pelo proxy de origem."
},
"role_map": {
"label": "Mapeamento de funções",
"description": "Mapeia os valores de grupo do proxy de origem para funções do Frigate (por exemplo, mapear grupos de administradores para a função admin)."
}
},
"description": "Configurações para integrar o Frigate atrás de um proxy reverso que repassa cabeçalhos de usuário autenticado.",
"logout_url": {
"label": "URL de logout",
"description": "URL para a qual os usuários são redirecionados ao sair pelo proxy."
},
"auth_secret": {
"label": "Segredo do proxy",
"description": "Segredo opcional verificado no cabeçalho X-Proxy-Secret para validar proxies confiáveis."
},
"default_role": {
"description": "Função padrão atribuída aos usuários autenticados por proxy quando nenhum mapeamento de função se aplica. Defina como 'none' para negar acesso a usuários não mapeados.",
"label": "Função padrão"
},
"separator": {
"label": "Caractere separador",
"description": "Caractere usado para dividir vários valores fornecidos nos cabeçalhos do proxy."
}
},
"telemetry": {
"label": "Telemetria",
"description": "Opções de telemetria e estatísticas do sistema, incluindo o monitoramento de GPU e de largura de banda de rede.",
"network_interfaces": {
"label": "Interfaces de rede",
"description": "Lista de prefixos de nomes de interfaces de rede a serem monitoradas para as estatísticas de largura de banda."
},
"stats": {
"label": "Estatísticas do sistema",
"description": "Opções para habilitar/desabilitar a coleta de várias estatísticas do sistema e da GPU.",
"amd_gpu_stats": {
"label": "Estatísticas da GPU AMD",
"description": "Habilita a coleta de estatísticas da GPU AMD, se houver uma GPU AMD presente."
},
"intel_gpu_stats": {
"label": "Estatísticas da GPU Intel",
"description": "Habilita a coleta de estatísticas da GPU Intel, se houver uma GPU Intel presente."
},
"network_bandwidth": {
"label": "Largura de banda de rede",
"description": "Habilita o monitoramento da largura de banda de rede por processo para os processos ffmpeg das câmeras e para os detectores (requer capabilities)."
},
"intel_gpu_device": {
"description": "Endereço do barramento PCI ou caminho do dispositivo DRM (por ex., /dev/dri/card1) usado para fixar as estatísticas da GPU Intel em um dispositivo específico quando houver vários.",
"label": "Dispositivo da GPU Intel"
}
},
"version_check": {
"description": "Habilita uma verificação externa para detectar se há uma versão mais recente do Frigate disponível.",
"label": "Verificação de versão"
}
},
"tls": {
"label": "TLS",
"description": "Configurações de TLS para os endpoints web do Frigate (porta 8971).",
"enabled": {
"label": "Habilitar TLS",
"description": "Habilita o TLS para a interface web e a API do Frigate na porta TLS configurada."
}
},
"genai": {
"label": "Configuração de IA generativa",
"description": "Configurações dos provedores de IA generativa integrados, usados para gerar descrições de objetos e resumos de revisão.",
"api_key": {
"label": "Chave de API",
"description": "Chave de API exigida por alguns provedores (também pode ser definida por variáveis de ambiente)."
},
"base_url": {
"label": "URL base",
"description": "URL base para provedores auto-hospedados ou compatíveis (por exemplo, uma instância do Ollama)."
},
"model": {
"label": "Modelo",
"description": "O modelo do provedor a ser usado para gerar descrições ou resumos."
},
"provider": {
"label": "Provedor",
"description": "O provedor de IA generativa a ser usado (por exemplo: ollama, gemini, openai)."
},
"roles": {
"label": "Funções",
"description": "Funções da IA generativa (chat, descriptions, embeddings, transcribe); um provedor por função. Apenas chat, descriptions e embeddings são concedidas por padrão; transcribe deve ser listada explicitamente."
},
"provider_options": {
"label": "Opções do provedor",
"description": "Opções adicionais específicas do provedor a serem passadas ao cliente de IA generativa."
},
"runtime_options": {
"label": "Opções de execução",
"description": "Opções de execução passadas ao provedor a cada chamada de inferência."
}
},
"classification": {
"label": "Classificação de objetos",
"description": "Configurações dos modelos de classificação usados para refinar os rótulos de objetos ou classificar estados.",
"bird": {
"label": "Configuração da classificação de pássaros",
"description": "Configurações específicas dos modelos de classificação de pássaros.",
"enabled": {
"label": "Classificação de pássaros",
"description": "Habilita ou desabilita a classificação de pássaros."
},
"threshold": {
"description": "Pontuação mínima de classificação necessária para aceitar uma classificação de pássaro.",
"label": "Pontuação mínima"
}
},
"custom": {
"label": "Modelos de classificação personalizados",
"enabled": {
"label": "Habilitar modelo",
"description": "Habilita ou desabilita o modelo de classificação personalizado."
},
"description": "Configuração dos modelos de classificação personalizados usados para detecção de objetos ou de estados.",
"name": {
"label": "Nome do modelo",
"description": "Identificador do modelo de classificação personalizado a ser usado."
},
"threshold": {
"label": "Limite de pontuação",
"description": "Limite de pontuação usado para alterar o estado da classificação."
},
"save_attempts": {
"label": "Tentativas a salvar",
"description": "Quantas tentativas de classificação salvar para a interface de classificações recentes."
},
"object_config": {
"objects": {
"label": "Classificar objetos",
"description": "Lista de tipos de objeto nos quais executar a classificação de objetos."
},
"classification_type": {
"description": "Tipo de classificação aplicado: 'sub_label' (adiciona um sub_label) ou outros tipos compatíveis.",
"label": "Tipo de classificação"
}
},
"state_config": {
"cameras": {
"label": "Câmeras de classificação",
"description": "Recorte e configurações por câmera para executar a classificação de estados.",
"crop": {
"label": "Recorte da classificação",
"description": "Coordenadas do recorte a serem usadas para executar a classificação nesta câmera."
}
},
"motion": {
"label": "Executar ao detectar movimento",
"description": "Se verdadeiro, executa a classificação quando movimento for detectado dentro do recorte especificado."
},
"interval": {
"label": "Intervalo de classificação",
"description": "Intervalo (em segundos) entre as execuções periódicas de classificação de estados."
}
}
}
},
"camera_groups": {
"description": "Configuração dos grupos de câmeras nomeados usados para organizar as câmeras na interface.",
"cameras": {
"label": "Lista de câmeras",
"description": "Lista dos nomes das câmeras incluídas neste grupo."
},
"label": "Grupos de câmeras",
"icon": {
"label": "Ícone do grupo",
"description": "Ícone usado para representar o grupo de câmeras na interface."
},
"order": {
"label": "Ordem de classificação",
"description": "Ordem numérica usada para ordenar os grupos de câmeras na interface; números maiores aparecem depois."
}
},
"camera_mqtt": {
"label": "MQTT",
"description": "Configurações de publicação de imagens via MQTT.",
"enabled": {
"description": "Habilita a publicação de snapshots dos objetos nos tópicos MQTT para esta câmera.",
"label": "Enviar imagem"
},
"timestamp": {
"label": "Adicionar carimbo de data/hora",
"description": "Sobrepõe um carimbo de data/hora nas imagens publicadas no MQTT."
},
"bounding_box": {
"label": "Adicionar caixa delimitadora",
"description": "Desenha caixas delimitadoras nas imagens publicadas via MQTT."
},
"crop": {
"label": "Recortar imagem",
"description": "Recorta as imagens publicadas no MQTT para a caixa delimitadora do objeto detectado."
},
"height": {
"label": "Altura da imagem",
"description": "Altura (em pixels) para redimensionar as imagens publicadas via MQTT."
},
"required_zones": {
"description": "Zonas em que um objeto deve entrar para que uma imagem seja publicada no MQTT.",
"label": "Zonas obrigatórias"
},
"quality": {
"label": "Qualidade JPEG",
"description": "Qualidade JPEG das imagens publicadas no MQTT (0-100)."
}
},
"active_profile": {
"label": "Perfil ativo",
"description": "Nome do perfil atualmente ativo. Apenas em tempo de execução, não é gravado no YAML."
},
"models": {
"label": "Modelos de detecção",
"scene": {
"label": "Cena do modelo",
"description": "O ambiente da câmera para o qual este modelo é usado. As câmeras selecionam um modelo definindo detect.scene com um valor correspondente, e 'all' é usado por qualquer câmera que não defina um."
},
"description": "Modelos de detecção de objetos e o hardware em que cada um é executado. As câmeras escolhem um modelo fazendo a correspondência entre o detect.scene delas e o scene do modelo.",
"devices": {
"label": "Hardware de detecção",
"description": "Hardware em que este modelo é executado, no formato '<detector>' ou '<detector>:<device>' (por exemplo, 'edgetpu:pci:0' ou 'openvino:GPU'). Listar o mesmo dispositivo mais de uma vez executa processos de inferência adicionais nele."
},
"path": {
"label": "Caminho do modelo personalizado do detector de objetos",
"description": "Caminho para um arquivo de modelo de detecção personalizado (ou plus://<model_id> para modelos do Frigate+)."
},
"labelmap_path": {
"description": "Caminho para um arquivo de labelmap que associa classes numéricas a rótulos de texto para o detector.",
"label": "Mapa de rótulos do detector de objetos personalizado"
},
"width": {
"label": "Largura de entrada do modelo de detecção de objetos",
"description": "Largura do tensor de entrada do modelo, em pixels."
},
"height": {
"label": "Altura de entrada do modelo de detecção de objetos",
"description": "Altura do tensor de entrada do modelo, em pixels."
},
"labelmap": {
"label": "Personalização do labelmap",
"description": "Substituições ou remapeamentos de entradas a serem mesclados ao labelmap padrão."
},
"attributes_map": {
"label": "Mapa de rótulos de objetos para seus rótulos de atributos",
"description": "Mapeamento de rótulos de objetos para rótulos de atributos usado para anexar metadados (por exemplo, 'car' -> ['license_plate'])."
},
"input_tensor": {
"label": "Formato do tensor de entrada do modelo",
"description": "Formato de tensor esperado pelo modelo: 'nhwc' ou 'nchw'."
},
"input_pixel_format": {
"label": "Formato de cor dos pixels de entrada do modelo",
"description": "Espaço de cor dos pixels esperado pelo modelo: 'rgb', 'bgr' ou 'yuv'."
},
"input_dtype": {
"label": "Tipo de dado de entrada do modelo",
"description": "Tipo de dado do tensor de entrada do modelo (por exemplo, 'float32')."
},
"model_type": {
"label": "Tipo de modelo de detecção de objetos",
"description": "Tipo de arquitetura do modelo detector (ssd, yolox, yolonas, yolo-generic, rfdetr, dfine), usado por alguns detectores para otimização."
}
"label": "UI"
}
}
@@ -134,85 +134,7 @@
"wizard": {
"title": "Criar nova classificação",
"steps": {
"nameAndDefine": "Nomear e definir",
"stateArea": "Área de estado",
"chooseExamples": "Escolher exemplos"
},
"step1": {
"description": "Modelos de estado monitoram áreas fixas da câmera em busca de mudanças (por ex., porta aberta/fechada). Modelos de objeto adicionam classificações aos objetos detectados (por ex., animais conhecidos, entregadores etc.).",
"name": "Nome",
"namePlaceholder": "Digite o nome do modelo...",
"type": "Tipo",
"typeState": "Estado",
"typeObject": "Objeto",
"objectLabel": "Rótulo do objeto",
"objectLabelPlaceholder": "Selecione o tipo de objeto...",
"classificationType": "Tipo de classificação",
"classificationTypeTip": "Saiba mais sobre os tipos de classificação",
"classificationTypeDesc": "Sub-rótulos adicionam texto extra ao rótulo do objeto (por ex., 'Pessoa: UPS'). Atributos são metadados pesquisáveis armazenados separadamente nos metadados do objeto.",
"classificationSubLabel": "Sub-rótulo",
"classificationAttribute": "Atributo",
"classes": "Classes",
"classesTip": "Saiba mais sobre as classes",
"classesStateDesc": "Defina os diferentes estados em que a área da sua câmera pode estar. Por exemplo: 'aberto' e 'fechado' para um portão de garagem.",
"classesObjectDesc": "Defina as diferentes categorias em que os objetos detectados serão classificados. Por exemplo: 'entregador', 'morador', 'estranho' para a classificação de pessoas.",
"classPlaceholder": "Digite o nome da classe...",
"errors": {
"nameRequired": "O nome do modelo é obrigatório",
"nameLength": "O nome do modelo deve ter no máximo 64 caracteres",
"nameOnlyNumbers": "O nome do modelo não pode conter apenas números",
"classRequired": "É necessária pelo menos 1 classe",
"classesUnique": "Os nomes das classes devem ser únicos",
"stateRequiresTwoClasses": "Modelos de estado exigem pelo menos 2 classes",
"objectLabelRequired": "Selecione um rótulo de objeto",
"objectTypeRequired": "Selecione um tipo de classificação",
"noneNotAllowed": "A classe 'none' não é permitida"
},
"states": "Estados"
},
"step2": {
"description": "Selecione as câmeras e defina a área a ser monitorada em cada uma. O modelo classificará o estado dessas áreas.",
"cameras": "Câmeras",
"selectCamera": "Selecionar câmera",
"noCameras": "Clique em + para adicionar câmeras",
"selectCameraPrompt": "Selecione uma câmera da lista para definir sua área de monitoramento"
},
"step3": {
"selectImagesPrompt": "Selecione todas as imagens com: {{className}}",
"selectImagesDescription": "Clique nas imagens para selecioná-las. Clique em Continuar quando terminar esta classe.",
"generating": {
"title": "Gerando imagens de amostra",
"description": "O Frigate está extraindo imagens representativas das suas gravações. Isso pode levar um momento..."
},
"training": {
"title": "Treinando modelo",
"description": "Seu modelo está sendo treinado em segundo plano. Feche esta janela e o modelo começará a funcionar assim que o treinamento for concluído."
},
"retryGenerate": "Tentar gerar novamente",
"noImages": "Nenhuma imagem de amostra gerada",
"classifying": "Classificando e treinando...",
"trainingStarted": "Treinamento iniciado com sucesso",
"errors": {
"noCameras": "Nenhuma câmera configurada",
"noObjectLabel": "Nenhum rótulo de objeto selecionado",
"generateFailed": "Falha ao gerar exemplos: {{error}}",
"generationFailed": "A geração falhou. Tente novamente.",
"classifyFailed": "Falha ao classificar as imagens: {{error}}"
},
"generateSuccess": "Imagens de amostra geradas com sucesso",
"allImagesRequired_one": "Classifique todas as imagens. Falta {{count}} imagem.",
"allImagesRequired_many": "Classifique todas as imagens. Faltam {{count}} de imagens.",
"allImagesRequired_other": "Classifique todas as imagens. Faltam {{count}} imagens.",
"modelCreated": "Modelo criado com sucesso. Use a visualização de Classificações recentes para adicionar imagens dos estados que faltam e, em seguida, treine o modelo.",
"missingStatesWarning": {
"description": "Nem todas as classes têm exemplos. Tente gerar novos exemplos para encontrar a classe ausente, ou continue e use a visualização de Classificações recentes para adicionar imagens depois.",
"title": "Exemplos de classes ausentes"
},
"refreshExamples": "Gerar novos exemplos",
"refreshConfirm": {
"title": "Gerar novos exemplos?",
"description": "Isso gerará um novo conjunto de imagens e limpará todas as seleções, incluindo as classes anteriores. Você precisará selecionar novamente os exemplos de todas as classes."
}
"nameAndDefine": "Nomear e definir"
}
},
"disabled": "Desativado"
+4 -34
View File
@@ -25,9 +25,7 @@
"noFoundForTimePeriod": "Nenhum evento encontrado neste período."
},
"recordings": {
"documentTitle": "Gravações - Frigate",
"invalidSharedLink": "Não foi possível abrir o link da gravação com carimbo de data/hora devido a um erro de análise.",
"invalidSharedCamera": "Não foi possível abrir o link da gravação com carimbo de data/hora devido a uma câmera desconhecida ou não autorizada."
"documentTitle": "Gravações - Frigate"
},
"calendarFilter": {
"last24Hours": "Últimas 24 horas"
@@ -37,10 +35,10 @@
"button": "Novos Itens para Revisar",
"label": "Ver novos itens para revisão"
},
"selected_one": "{{count}} selecionados",
"selected_one": "{{count}} selecionado(s)",
"documentTitle": "Revisar - Frigate",
"markAsReviewed": "Marcar como Revisado",
"selected_other": "{{count}} selecionados",
"selected_other": "{{count}} selecionado(s)",
"camera": "Câmera",
"detected": "detectado",
"detail": {
@@ -64,33 +62,5 @@
"zoomOut": "Diminuir o zoom",
"select_all": "Todos",
"normalActivity": "Normal",
"needsReview": "Precisa de revisão",
"securityConcern": "Questão de segurança",
"motionSearch": {
"menuItem": "Busca de movimento",
"openMenu": "Opções da câmera"
},
"motionPreviews": {
"menuItem": "Ver prévias de movimento",
"title": "Prévias de movimento: {{camera}}",
"mobileSettingsTitle": "Configurações da prévia de movimento",
"mobileSettingsDesc": "Ajuste a velocidade de reprodução, o escurecimento e o recorte, e escolha uma data para revisar clipes apenas com movimento.",
"dimAria": "Ajustar a intensidade do escurecimento",
"dim": "Escurecer",
"speed": "Velocidade",
"dimDesc": "Aumente o escurecimento para aumentar a visibilidade da área de movimento.",
"speedAria": "Selecione a velocidade de reprodução da prévia",
"speedDesc": "Escolha a rapidez com que os clipes de prévia são reproduzidos.",
"back": "Voltar",
"empty": "Nenhuma prévia disponível",
"noPreview": "Prévia indisponível",
"seekAria": "Ir para {{time}} no player de {{camera}}",
"filterDesc": "Selecione áreas para mostrar apenas os clipes com movimento nessas regiões.",
"filterClear": "Limpar",
"filter": "Filtro",
"crop": "Recortar para o filtro",
"cropAria": "Alterna o recorte das prévias para as áreas filtradas",
"cropDesc": "Amplia as prévias nas áreas de filtro selecionadas em vez de mostrar o quadro completo."
},
"subOnlyQuality": "Apenas gravações de baixa qualidade estão disponíveis neste intervalo de tempo"
"needsReview": "Precisa de revisão"
}
+6 -58
View File
@@ -49,15 +49,13 @@
"regenerate": "Uma nova descrição foi solicitada do {{provider}}. Dependendo da velocidade do seu fornecedor, a nova descrição pode levar algum tempo para regenerar.",
"updatedSublabel": "Sub-rótulo atualizado com sucesso.",
"updatedLPR": "Placa de identificação atualizada com sucesso.",
"audioTranscription": "Transcrição de áudio requisitada com sucesso. Dependendo da velocidade de seu servidor Frigate, a transcrição pode demorar um tempo para completar.",
"updatedAttributes": "Atributos atualizados com sucesso."
"audioTranscription": "Transcrição de áudio requisitada com sucesso. Dependendo da velocidade de seu servidor Frigate, a transcrição pode demorar um tempo para completar."
},
"error": {
"regenerate": "Falha ao ligar para {{provider}} para uma descrição nova: {{errorMessage}}",
"updatedSublabelFailed": "Falha ao atualizar sub-rótulo: {{errorMessage}}",
"updatedLPRFailed": "Falha ao atualizar placa de identificação: {{errorMessage}}",
"audioTranscription": "Falha ao requisitar transcrição de áudio: {{errorMessage}}",
"updatedAttributesFailed": "Falha ao atualizar os atributos: {{errorMessage}}"
"audioTranscription": "Falha ao requisitar transcrição de áudio: {{errorMessage}}"
}
}
},
@@ -105,16 +103,7 @@
},
"score": {
"label": "Pontuação"
},
"editAttributes": {
"title": "Editar atributos",
"desc": "Selecione os atributos de classificação para este(a) {{label}}"
},
"title": {
"label": "Título"
},
"attributes": "Atributos de classificação",
"scoreInfo": "Informações da pontuação"
}
},
"trackedObjectDetails": "Detalhes do Objeto Rastreado",
"type": {
@@ -158,36 +147,12 @@
"audioTranscription": {
"label": "Transcrever",
"aria": "Solicitar transcrição de áudio"
},
"showObjectDetails": {
"label": "Mostrar trajeto do objeto"
},
"hideObjectDetails": {
"label": "Ocultar trajeto do objeto"
},
"viewTrackingDetails": {
"label": "Ver detalhes do rastreamento",
"aria": "Mostrar os detalhes do rastreamento"
},
"downloadCleanSnapshot": {
"aria": "Baixar snapshot limpo",
"label": "Baixar snapshot limpo"
},
"debugReplay": {
"label": "Replay de depuração",
"aria": "Ver este objeto rastreado na visualização de replay de depuração"
},
"more": {
"aria": "Mais"
}
},
"dialog": {
"confirmDelete": {
"title": "Confirmar Exclusão",
"desc": "Deletar esse objeto rastreado remove a captura de imagem, os embeddings salvos, e os detalhes de rastreamento associados. Gravações desse objeto rastreado na visualização de Histórico <em>NÃO</em> serão deletadas.<br /><br />Tem certeza que deseja prosseguir?"
},
"toast": {
"error": "Erro ao excluir este objeto rastreado: {{errorMessage}}"
}
},
"noTrackedObjects": "Nenhum Objeto Rastreado Encontrado",
@@ -202,9 +167,7 @@
"success": "Objeto rastreado deletado com sucesso.",
"error": "Falha ao detectar objeto rastreado {{errorMessage}}"
}
},
"previousTrackedObject": "Objeto rastreado anterior",
"nextTrackedObject": "Próximo objeto rastreado"
}
},
"aiAnalysis": {
"title": "Análise de IA"
@@ -228,29 +191,14 @@
"header": {
"zones": "Zonas",
"area": "Área",
"ratio": "Proporção",
"score": "Pontuação",
"computedScore": "Pontuação calculada",
"topScore": "Maior pontuação",
"toggleAdvancedScores": "Alternar pontuações avançadas"
"ratio": "Proporção"
}
},
"title": "Detalhes de Rastreamento",
"createObjectMask": "Criar máscara de objeto",
"annotationSettings": {
"showAllZones": {
"title": "Mostrar todas as Zonas",
"desc": "Sempre mostrar zonas nos quadros em que os objetos entraram em uma zona."
},
"title": "Configurações de anotação",
"offset": {
"label": "Deslocamento de anotação",
"desc": "Esses dados vêm do feed de detecção da sua câmera, mas são sobrepostos às imagens do feed de gravação. É improvável que os dois streams estejam perfeitamente sincronizados. Como resultado, a caixa delimitadora e a filmagem não se alinharão perfeitamente. Você pode usar esta configuração para deslocar as anotações para frente ou para trás no tempo, alinhando-as melhor com a filmagem gravada.",
"millisecondsToOffset": "Milissegundos para deslocar as anotações de detecção. <em>Padrão: 0</em>",
"tips": "Diminua o valor se a reprodução do vídeo estiver adiantada em relação às caixas e aos pontos do trajeto, e aumente o valor se a reprodução estiver atrasada em relação a eles. Este valor pode ser negativo.",
"toast": {
"success": "O deslocamento de anotação de {{camera}} foi salvo no arquivo de configuração."
}
"title": "Mostrar todas as Zonas"
}
},
"carousel": {
+2 -91
View File
@@ -14,9 +14,7 @@
"toast": {
"error": {
"renameExportFailed": "Falha ao renomear exportação: {{errorMessage}}",
"assignCaseFailed": "Falha ao atualizar atribuição ao caso: {{errorMessage}}",
"caseSaveFailed": "Falha ao salvar o caso: {{errorMessage}}",
"caseDeleteFailed": "Falha ao excluir o caso: {{errorMessage}}"
"assignCaseFailed": "Falha ao atualizar atribuição ao caso: {{errorMessage}}"
}
},
"tooltip": {
@@ -38,92 +36,5 @@
"newCaseOption": "Criar novo caso",
"nameLabel": "Nome do caso",
"descriptionLabel": "Descrição"
},
"toolbar": {
"newCase": "Novo caso",
"addExport": "Adicionar exportação",
"deleteCase": "Excluir caso",
"editCase": "Editar caso"
},
"deleteCase": {
"label": "Excluir caso",
"desc": "Tem certeza de que deseja excluir {{caseName}}?",
"descKeepExports": "As exportações continuarão disponíveis como exportações sem categoria.",
"deleteExports": "Excluir também as exportações",
"descDeleteExports": "Todas as exportações deste caso serão excluídas permanentemente."
},
"caseCard": {
"emptyCase": "Nenhuma exportação ainda"
},
"jobCard": {
"defaultName": "Exportação de {{camera}}",
"queued": "Na fila",
"running": "Em execução",
"preparing": "Preparando",
"encoding": "Codificando",
"encodingRetry": "Codificando (nova tentativa)",
"finalizing": "Finalizando",
"copying": "Copiando",
"merging": "Mesclando"
},
"caseView": {
"noDescription": "Sem descrição",
"createdAt": "Criado {{value}}",
"exportCount_one": "1 exportação",
"exportCount_other": "{{count}} exportações",
"cameraCount_one": "1 câmera",
"showMore": "Mostrar mais",
"showLess": "Mostrar menos",
"emptyTitle": "Este caso está vazio",
"cameraCount_other": "{{count}} câmeras",
"emptyDescription": "Adicione exportações sem categoria existentes para manter o caso organizado.",
"emptyDescriptionNoExports": "Ainda não há exportações sem categoria disponíveis para adicionar."
},
"bulkActions": {
"deleteNow": "Excluir agora",
"addToCase": "Adicionar ao caso",
"moveToCase": "Mover para o caso",
"delete": "Excluir",
"removeFromCase": "Remover do caso"
},
"bulkDelete": {
"desc_one": "Tem certeza de que deseja excluir {{count}} exportação?",
"title": "Excluir exportações",
"desc_other": "Tem certeza de que deseja excluir {{count}} exportações?"
},
"bulkRemoveFromCase": {
"title": "Remover do caso",
"desc_other": "Remover {{count}} exportações deste caso?",
"descKeepExports": "As exportações serão movidas para sem categoria.",
"desc_one": "Remover {{count}} exportação deste caso?",
"descDeleteExports": "As exportações serão excluídas permanentemente.",
"deleteExports": "Excluir as exportações em vez disso"
},
"caseEditor": {
"createTitle": "Criar caso",
"editTitle": "Editar caso",
"descriptionPlaceholder": "Adicione notas ou contexto para este caso",
"namePlaceholder": "Nome do caso"
},
"addExportDialog": {
"searchPlaceholder": "Pesquisar exportações sem categoria",
"title": "Adicionar exportação a {{caseName}}",
"empty": "Nenhuma exportação sem categoria corresponde a esta pesquisa.",
"addButton_one": "Adicionar 1 exportação",
"addButton_other": "Adicionar {{count}} exportações",
"adding": "Adicionando..."
},
"bulkToast": {
"success": {
"delete": "Exportações excluídas com sucesso",
"remove": "Exportações removidas do caso com sucesso",
"reassign": "Atribuição de caso atualizada com sucesso"
},
"error": {
"deleteFailed": "Falha ao excluir as exportações: {{errorMessage}}",
"reassignFailed": "Falha ao atualizar a atribuição de caso: {{errorMessage}}"
}
},
"selected_one": "{{count}} selecionados",
"selected_other": "{{count}} selecionados"
}
}
+3 -34
View File
@@ -6,12 +6,7 @@
"lowBandwidthMode": "Modo de baixa largura de banda",
"twoWayTalk": {
"enable": "Habilitar Fala em Dois Sentidos",
"disable": "Desabilitar Fala em Dois Sentidos",
"error": {
"microphone": "A conversa bidirecional não conseguiu acessar o seu microfone",
"refused": "A conversa bidirecional falhou ao iniciar. Consulte o console do navegador para mais detalhes."
},
"requiresWebRTC": "A conversa bidirecional exige WebRTC, que não está disponível"
"disable": "Desabilitar Fala em Dois Sentidos"
},
"cameraAudio": {
"enable": "Habilitar Áudio da Câmera",
@@ -136,40 +131,14 @@
},
"lowBandwidth": {
"tips": "A transmissão ao vivo está em modo de economia de dados devido a erros de buffering ou de transmissão.",
"resetStream": "Resetar transmissão",
"force": {
"label": "Forçar modo de baixa largura de banda",
"desc": "Sempre reproduz o feed de baixa largura de banda integrado do Frigate em vez do stream selecionado. Funciona em qualquer conexão, mas tem qualidade inferior e não tem áudio."
}
"resetStream": "Resetar transmissão"
},
"playInBackground": {
"label": "Reproduzir em segundo plano",
"tips": "Habilitar essa opção para continuar a transmissão quando o reprodutor estiver oculto."
},
"debug": {
"picker": "A seleção da transmissão fica indisponível em modo de depuração. A visualização de depuração sempre usa o papel de detecção atribuído à transmissão.",
"technology": "A seleção da tecnologia de stream não está disponível no modo de depuração."
},
"mode": "Tecnologia de streaming",
"technology": {
"name": {
"mse": "MSE",
"webrtc": "WebRTC",
"jsmpeg": "JSMpeg"
},
"description": "Escolha a sua tecnologia de streaming preferida. O Frigate ainda pode recorrer ao modo de baixa largura de banda em caso de erros de reprodução ou de rede.",
"tips": {
"mse": "Padrão recomendado, ampla compatibilidade e reprodução fluida",
"webrtc": "Requer configuração adicional e não é compatível com todos os dispositivos"
},
"unavailable": {
"not-configured": "O WebRTC não está configurado. Defina candidates ou ice_servers do WebRTC no go2rtc.",
"unreachable": "O WebRTC não conseguiu se conectar. Verifique se a porta 8555 está acessível e se qualquer servidor STUN/TURN está correto.",
"video-codec": "O codec de vídeo deste stream não é compatível com o WebRTC neste navegador.",
"audio-codec": "O codec de áudio deste stream não é compatível com o WebRTC. Transcodifique para opus ou G.711 com o go2rtc.",
"checking": "Verificando a disponibilidade do WebRTC…",
"browser": "Seu navegador não é compatível com WebRTC."
}
"picker": "A seleção da transmissão fica indisponível em modo de depuração. A visualização de depuração sempre usa o papel de detecção atribuído à transmissão."
}
},
"cameraSettings": {
@@ -1,80 +1 @@
{
"title": "Busca de movimento",
"documentTitle": "Busca de movimento - Frigate",
"description": "Desenhe um polígono para definir a região de interesse e especifique um intervalo de tempo para buscar mudanças de movimento nessa região.",
"startSearch": "Iniciar busca",
"searchStarted": "Busca iniciada",
"searchCancelled": "Busca cancelada",
"cancelSearch": "Cancelar",
"selectCamera": "A busca de movimento está carregando",
"searching": "Busca em andamento.",
"noResultsYet": "Execute uma busca para encontrar mudanças de movimento na região selecionada",
"noChangesFound": "Nenhuma alteração de pixels detectada na região selecionada",
"searchComplete": "Busca concluída",
"framesProcessed": "{{count}} quadros processados",
"changesFound_one": "Encontrada {{count}} mudança de movimento",
"changesFound_many": "Encontradas {{count}} mudanças de movimento",
"changesFound_other": "Encontradas {{count}} mudanças de movimento",
"jumpToTime": "Ir para este momento",
"results": "Resultados",
"showSegmentHeatmap": "Mapa de calor",
"newSearch": "Nova busca",
"clearResults": "Limpar resultados",
"clearROI": "Limpar polígono",
"polygonControls": {
"points_one": "{{count}} ponto",
"points_many": "{{count}} pontos",
"points_other": "{{count}} pontos",
"undo": "Desfazer último ponto",
"reset": "Redefinir polígono",
"drawMode": "Desenhar",
"moveMode": "Mover"
},
"motionHeatmapLabel": "Mapa de calor de movimento",
"dialog": {
"title": "Busca de movimento",
"cameraLabel": "Câmera",
"previewAlt": "Prévia da câmera {{camera}}"
},
"timeRange": {
"title": "Intervalo da busca",
"start": "Hora de início",
"end": "Hora de término"
},
"settings": {
"title": "Configurações da busca",
"parallelMode": "Modo paralelo",
"parallelModeDesc": "Verifica vários intervalos de gravação ao mesmo tempo (mais rápido; usa mais recursos de decodificação)",
"threshold": "Limite de sensibilidade",
"thresholdDesc": "Valores menores detectam mudanças menores (1-255)",
"minArea": "Área mínima de mudança",
"minAreaDesc": "Tamanho mínimo de uma única região em movimento, como porcentagem da região de interesse",
"maxResultsDesc": "Parar após esta quantidade de carimbos de data/hora correspondentes",
"maxResults": "Máximo de resultados"
},
"errors": {
"noCamera": "Selecione uma câmera",
"noROI": "Desenhe uma região de interesse",
"noTimeRange": "Selecione um intervalo de tempo",
"invalidTimeRange": "A hora de término deve ser posterior à hora de início",
"polygonTooSmall": "O polígono deve ter pelo menos 3 pontos",
"searchFailed": "Falha na busca: {{message}}",
"unknown": "Erro desconhecido"
},
"changePercentage": "{{percentage}}% alterado",
"metrics": {
"title": "Métricas da busca",
"segmentsScanned": "Segmentos verificados",
"segmentsProcessed": "Processados",
"segmentsSkippedInactive": "Ignorados (sem atividade)",
"segmentsSkippedHeatmap": "Ignorados (sem sobreposição com a ROI)",
"fallbackFullRange": "Varredura completa de contingência",
"framesDecoded": "Quadros decodificados",
"wallTime": "Tempo de busca",
"segmentErrors": "Erros de segmento",
"seconds": "{{seconds}}s",
"minutesSeconds": "{{minutes}}min {{seconds}}s",
"scanSummary": "{{segments}} segmentos · {{time}}"
},
"scanning": "Verificando {{time}}"
}
{}
+1 -42
View File
@@ -14,47 +14,6 @@
"custom": "Personalizada"
},
"startButton": "Iniciar Reprodução",
"selectFromTimeline": "Selecionar",
"startLabel": "Início",
"endLabel": "Fim",
"toast": {
"error": "Falha ao iniciar o replay de depuração: {{error}}",
"stopError": "Falha ao interromper o replay de depuração: {{error}}",
"goToReplay": "Ir para o replay",
"alreadyActive": "Já existe uma sessão de replay ativa",
"noRecordings": "Nenhuma gravação encontrada no intervalo de tempo selecionado"
},
"starting": "Iniciando replay..."
},
"page": {
"noSession": "Nenhuma sessão de replay de depuração ativa",
"noSessionDesc": "Inicie um replay de depuração na visualização de Histórico clicando no botão Ações da barra de ferramentas e escolhendo Replay de depuração.",
"goToRecordings": "Ir para o histórico",
"sourceCamera": "Câmera de origem",
"initializingReplay": "Inicializando replay de depuração...",
"stoppingReplay": "Interrompendo replay de depuração...",
"replayCamera": "Câmera de replay",
"confirmStop": {
"title": "Interromper replay de depuração?",
"description": "Isso interromperá a sessão e limpará todos os dados temporários. Tem certeza?",
"confirm": "Interromper replay",
"cancel": "Cancelar"
},
"stopReplay": "Interromper replay",
"activity": "Atividade",
"objects": "Lista de objetos",
"audioDetections": "Detecções de áudio",
"activeTracking": "Rastreamento ativo",
"noActivity": "Nenhuma atividade detectada",
"noActiveTracking": "Nenhum rastreamento ativo",
"configuration": "Configuração",
"configurationDesc": "Ajuste com precisão as configurações de detecção de movimento e rastreamento de objetos da câmera de replay de depuração. Nenhuma alteração é salva no seu arquivo de configuração do Frigate.",
"preparingClip": "Preparando o clipe…",
"preparingClipDesc": "O Frigate está juntando as gravações do intervalo de tempo selecionado. Isso pode levar um minuto para intervalos mais longos.",
"startingCamera": "Iniciando replay de depuração…",
"startError": {
"back": "Voltar ao histórico",
"title": "Falha ao iniciar o replay de depuração"
}
"selectFromTimeline": "Selecionar"
}
}

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