mirror of
https://github.com/blakeblackshear/frigate.git
synced 2026-03-27 02:28:22 +03:00
Compare commits
103 Commits
14fcaa9911
...
88bad3423b
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
88bad3423b | ||
|
|
f3cda9020b | ||
|
|
0c333ec28a | ||
|
|
de986c7430 | ||
|
|
dd2d7aca19 | ||
|
|
3f1bf1ae12 | ||
|
|
d6e8cad32f | ||
|
|
699d5ffa28 | ||
|
|
f400e91ede | ||
|
|
3bac4b15ae | ||
|
|
b2c424ad73 | ||
|
|
c18846ac62 | ||
|
|
b65ae76f0c | ||
|
|
5faf5e0d84 | ||
|
|
6837b9c89a | ||
|
|
f04df4a144 | ||
|
|
e42f70eeec | ||
|
|
e7b2b919d5 | ||
|
|
c68b7c9f46 | ||
|
|
8184ec5c8f | ||
|
|
ef448a7f7c | ||
|
|
f841ccdb63 | ||
|
|
4b6228acd9 | ||
|
|
0b8d1ce568 | ||
|
|
9ad7a2639f | ||
|
|
089c2c1018 | ||
|
|
3e97f9e985 | ||
|
|
eb9f16b4fa | ||
|
|
45c6be47d2 | ||
|
|
5a6c62a844 | ||
|
|
f29fbe14ca | ||
|
|
cc941ab2db | ||
|
|
56b3ebe791 | ||
|
|
6fdfe22f8c | ||
|
|
0cf713985f | ||
|
|
dc39d2f0ef | ||
|
|
e6387dac05 | ||
|
|
c870ebea37 | ||
|
|
56a1a0f5e3 | ||
|
|
67a245c8ef | ||
|
|
a072600c94 | ||
|
|
b603678b26 | ||
|
|
8793650c2f | ||
|
|
9c8dd9a6ba | ||
|
|
507b495b90 | ||
|
|
3525f32bc2 | ||
|
|
ac142449f1 | ||
|
|
47b89a1d60 | ||
|
|
cdcf56092c | ||
|
|
08ee2e21de | ||
|
|
9ab4dd4538 | ||
|
|
fe5441349b | ||
|
|
a4b1cc3a54 | ||
|
|
99e25661b2 | ||
|
|
20360db2c9 | ||
|
|
3826d72c2a | ||
|
|
3d5757c640 | ||
|
|
86100fde6f | ||
|
|
28b1195a79 | ||
|
|
b6db38bd4e | ||
|
|
92c6b8e484 | ||
|
|
9381f26352 | ||
|
|
e0180005be | ||
|
|
2041798702 | ||
|
|
3d23b5de30 | ||
|
|
209bb44518 | ||
|
|
88462cd6c3 | ||
|
|
c2cc23861a | ||
|
|
2b46084260 | ||
|
|
67466f215c | ||
|
|
e011424947 | ||
|
|
a1a0051dd7 | ||
|
|
ff331060c3 | ||
|
|
7aab1f02ec | ||
|
|
352d271fe4 | ||
|
|
a6e11a59d6 | ||
|
|
a7d8d13d9a | ||
|
|
4d51f7a1bb | ||
|
|
c9be98f935 | ||
|
|
85ed8c6432 | ||
|
|
f0d69f7856 | ||
|
|
5b16978430 | ||
|
|
b6142e3017 | ||
|
|
e1a6f69c4e | ||
|
|
d940ff3341 | ||
|
|
1f14f1cda0 | ||
|
|
806c5892b6 | ||
|
|
8bc82060f1 | ||
|
|
3cc8311b48 | ||
|
|
71139ef842 | ||
|
|
f4f32a3f59 | ||
|
|
29a4076589 | ||
|
|
d4d4164f99 | ||
|
|
5cc81bc7a1 | ||
|
|
1856e62ad0 | ||
|
|
252f1a6eb9 | ||
|
|
ad076aefff | ||
|
|
8a95cd2472 | ||
|
|
3aeeb09834 | ||
|
|
8c98b4c9d0 | ||
|
|
a2d6e04f45 | ||
|
|
7c11747ab3 | ||
|
|
5f2536dcd8 |
@ -1514,11 +1514,11 @@ RF-DETR can be exported as ONNX by running the command below. You can copy and p
|
||||
|
||||
```sh
|
||||
docker build . --build-arg MODEL_SIZE=Nano --rm --output . -f- <<'EOF'
|
||||
FROM python:3.11 AS build
|
||||
FROM python:3.12 AS build
|
||||
RUN apt-get update && apt-get install --no-install-recommends -y libgl1 && rm -rf /var/lib/apt/lists/*
|
||||
COPY --from=ghcr.io/astral-sh/uv:0.8.0 /uv /bin/
|
||||
COPY --from=ghcr.io/astral-sh/uv:0.10.4 /uv /bin/
|
||||
WORKDIR /rfdetr
|
||||
RUN uv pip install --system rfdetr[onnxexport] torch==2.8.0 onnx==1.19.1 onnxscript
|
||||
RUN uv pip install --system rfdetr[onnxexport] torch==2.8.0 onnx==1.19.1 transformers==4.57.6 onnxscript
|
||||
ARG MODEL_SIZE
|
||||
RUN python3 -c "from rfdetr import RFDETR${MODEL_SIZE}; x = RFDETR${MODEL_SIZE}(resolution=320); x.export(simplify=True)"
|
||||
FROM scratch
|
||||
|
||||
@ -9,4 +9,25 @@ Snapshots are accessible in the UI in the Explore pane. This allows for quick su
|
||||
|
||||
To only save snapshots for objects that enter a specific zone, [see the zone docs](./zones.md#restricting-snapshots-to-specific-zones)
|
||||
|
||||
Snapshots sent via MQTT are configured in the [config file](https://docs.frigate.video/configuration/) under `cameras -> your_camera -> mqtt`
|
||||
Snapshots sent via MQTT are configured in the [config file](/configuration) under `cameras -> your_camera -> mqtt`
|
||||
|
||||
## Frame Selection
|
||||
|
||||
Frigate does not save every frame — it picks a single "best" frame for each tracked object and uses it for both the snapshot and clean copy. As the object is tracked across frames, Frigate continuously evaluates whether the current frame is better than the previous best based on detection confidence, object size, and the presence of key attributes like faces or license plates. Frames where the object touches the edge of the frame are deprioritized. The snapshot is written to disk once tracking ends using whichever frame was determined to be the best.
|
||||
|
||||
MQTT snapshots are published more frequently — each time a better thumbnail frame is found during tracking, or when the current best image is older than `best_image_timeout` (default: 60s). These use their own annotation settings configured under `cameras -> your_camera -> mqtt`.
|
||||
|
||||
## Clean Copy
|
||||
|
||||
Frigate can produce up to two snapshot files per event, each used in different places:
|
||||
|
||||
| Version | File | Annotations | Used by |
|
||||
| --- | --- | --- | --- |
|
||||
| **Regular snapshot** | `<camera>-<id>.jpg` | Respects your `timestamp`, `bounding_box`, `crop`, and `height` settings | API (`/api/events/<id>/snapshot.jpg`), MQTT (`<camera>/<label>/snapshot`), Explore pane in the UI |
|
||||
| **Clean copy** | `<camera>-<id>-clean.webp` | Always unannotated — no bounding box, no timestamp, no crop, full resolution | API (`/api/events/<id>/snapshot-clean.webp`), [Frigate+](/plus/first_model) submissions, "Download Clean Snapshot" in the UI |
|
||||
|
||||
MQTT snapshots are configured separately under `cameras -> your_camera -> mqtt` and are unrelated to the clean copy.
|
||||
|
||||
The clean copy is required for submitting events to [Frigate+](/plus/first_model) — if you plan to use Frigate+, keep `clean_copy` enabled regardless of your other snapshot settings.
|
||||
|
||||
If you are not using Frigate+ and `timestamp`, `bounding_box`, and `crop` are all disabled, the regular snapshot is already effectively clean, so `clean_copy` provides no benefit and only uses additional disk space. You can safely set `clean_copy: False` in this case.
|
||||
|
||||
@ -185,7 +185,7 @@ On Raspberry Pi OS **Trixie**, the Hailo driver is no longer shipped with the ke
|
||||
|
||||
This command should return no results.
|
||||
|
||||
3. **Run the installation script**:
|
||||
2. **Run the installation script**:
|
||||
|
||||
Download the installation script:
|
||||
|
||||
@ -213,7 +213,7 @@ On Raspberry Pi OS **Trixie**, the Hailo driver is no longer shipped with the ke
|
||||
- Download and install the required firmware
|
||||
- Set up udev rules
|
||||
|
||||
4. **Reboot your system**:
|
||||
3. **Reboot your system**:
|
||||
|
||||
After the script completes successfully, reboot to load the firmware:
|
||||
|
||||
@ -221,7 +221,7 @@ On Raspberry Pi OS **Trixie**, the Hailo driver is no longer shipped with the ke
|
||||
sudo reboot
|
||||
```
|
||||
|
||||
5. **Verify the installation**:
|
||||
4. **Verify the installation**:
|
||||
|
||||
After rebooting, verify that the Hailo device is available:
|
||||
|
||||
@ -689,3 +689,42 @@ docker run \
|
||||
```
|
||||
|
||||
Log into QNAP, open Container Station. Frigate docker container should be listed under 'Overview' and running. Visit Frigate Web UI by clicking Frigate docker, and then clicking the URL shown at the top of the detail page.
|
||||
|
||||
## macOS - Apple Silicon
|
||||
|
||||
:::warning
|
||||
|
||||
macOS uses port 5000 for its Airplay Receiver service. If you want to expose port 5000 in Frigate for local app and API access the port will need to be mapped to another port on the host e.g. 5001
|
||||
|
||||
Failure to remap port 5000 on the host will result in the WebUI and all API endpoints on port 5000 being unreachable, even if port 5000 is exposed correctly in Docker.
|
||||
|
||||
:::
|
||||
|
||||
Docker containers on macOS can be orchestrated by either [Docker Desktop](https://docs.docker.com/desktop/setup/install/mac-install/) or [OrbStack](https://orbstack.dev) (native swift app). The difference in inference speeds is negligable, however CPU, power consumption and container start times will be lower on OrbStack because it is a native Swift application.
|
||||
|
||||
To allow Frigate to use the Apple Silicon Neural Engine / Processing Unit (NPU) the host must be running [Apple Silicon Detector](../configuration/object_detectors.md#apple-silicon-detector) on the host (outside Docker)
|
||||
|
||||
#### Docker Compose example
|
||||
```yaml
|
||||
services:
|
||||
frigate:
|
||||
container_name: frigate
|
||||
image: ghcr.io/blakeblackshear/frigate:stable-standard-arm64
|
||||
restart: unless-stopped
|
||||
shm_size: "512mb" # update for your cameras based on calculation above
|
||||
volumes:
|
||||
- /etc/localtime:/etc/localtime:ro
|
||||
- /path/to/your/config:/config
|
||||
- /path/to/your/recordings:/recordings
|
||||
ports:
|
||||
- "8971:8971"
|
||||
# If exposing on macOS map to a diffent host port like 5001 or any orher port with no conflicts
|
||||
# - "5001:5000" # Internal unauthenticated access. Expose carefully.
|
||||
- "8554:8554" # RTSP feeds
|
||||
extra_hosts:
|
||||
# This is very important
|
||||
# It allows frigate access to the NPU on Apple Silicon via Apple Silicon Detector
|
||||
- "host.docker.internal:host-gateway" # Required to talk to the NPU detector
|
||||
environment:
|
||||
- FRIGATE_RTSP_PASSWORD: "password"
|
||||
```
|
||||
|
||||
@ -16,7 +16,15 @@ See the [MQTT integration
|
||||
documentation](https://www.home-assistant.io/integrations/mqtt/) for more
|
||||
details.
|
||||
|
||||
In addition, MQTT must be enabled in your Frigate configuration file and Frigate must be connected to the same MQTT server as Home Assistant for many of the entities created by the integration to function.
|
||||
In addition, MQTT must be enabled in your Frigate configuration file and Frigate must be connected to the same MQTT server as Home Assistant for many of the entities created by the integration to function, e.g.:
|
||||
|
||||
```yaml
|
||||
mqtt:
|
||||
enabled: True
|
||||
host: mqtt.server.com # the address of your HA server that's running the MQTT integration
|
||||
user: your_mqtt_broker_username
|
||||
password: your_mqtt_broker_password
|
||||
```
|
||||
|
||||
### Integration installation
|
||||
|
||||
@ -95,12 +103,12 @@ services:
|
||||
|
||||
If you are using Home Assistant Add-on, the URL should be one of the following depending on which Add-on variant you are using. Note that if you are using the Proxy Add-on, you should NOT point the integration at the proxy URL. Just enter the same URL used to access Frigate directly from your network.
|
||||
|
||||
| Add-on Variant | URL |
|
||||
| -------------------------- | ----------------------------------------- |
|
||||
| Frigate | `http://ccab4aaf-frigate:5000` |
|
||||
| Frigate (Full Access) | `http://ccab4aaf-frigate-fa:5000` |
|
||||
| Frigate Beta | `http://ccab4aaf-frigate-beta:5000` |
|
||||
| Frigate Beta (Full Access) | `http://ccab4aaf-frigate-fa-beta:5000` |
|
||||
| Add-on Variant | URL |
|
||||
| -------------------------- | -------------------------------------- |
|
||||
| Frigate | `http://ccab4aaf-frigate:5000` |
|
||||
| Frigate (Full Access) | `http://ccab4aaf-frigate-fa:5000` |
|
||||
| Frigate Beta | `http://ccab4aaf-frigate-beta:5000` |
|
||||
| Frigate Beta (Full Access) | `http://ccab4aaf-frigate-fa-beta:5000` |
|
||||
|
||||
### Frigate running on a separate machine
|
||||
|
||||
|
||||
@ -120,7 +120,7 @@ Message published for each changed tracked object. The first message is publishe
|
||||
|
||||
### `frigate/tracked_object_update`
|
||||
|
||||
Message published for updates to tracked object metadata, for example:
|
||||
Message published for updates to tracked object metadata. All messages include an `id` field which is the tracked object's event ID, and can be used to look up the event via the API or match it to items in the UI.
|
||||
|
||||
#### Generative AI Description Update
|
||||
|
||||
@ -134,12 +134,14 @@ Message published for updates to tracked object metadata, for example:
|
||||
|
||||
#### Face Recognition Update
|
||||
|
||||
Published after each recognition attempt, regardless of whether the score meets `recognition_threshold`. See the [Face Recognition](/configuration/face_recognition) documentation for details on how scoring works.
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "face",
|
||||
"id": "1607123955.475377-mxklsc",
|
||||
"name": "John",
|
||||
"score": 0.95,
|
||||
"name": "John", // best matching person, or null if no match
|
||||
"score": 0.95, // running weighted average across all recognition attempts
|
||||
"camera": "front_door_cam",
|
||||
"timestamp": 1607123958.748393
|
||||
}
|
||||
@ -147,11 +149,13 @@ Message published for updates to tracked object metadata, for example:
|
||||
|
||||
#### License Plate Recognition Update
|
||||
|
||||
Published when a license plate is recognized on a car object. See the [License Plate Recognition](/configuration/license_plate_recognition) documentation for details.
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "lpr",
|
||||
"id": "1607123955.475377-mxklsc",
|
||||
"name": "John's Car",
|
||||
"name": "John's Car", // known name for the plate, or null
|
||||
"plate": "123ABC",
|
||||
"score": 0.95,
|
||||
"camera": "driveway_cam",
|
||||
|
||||
@ -3,12 +3,13 @@
|
||||
import base64
|
||||
import json
|
||||
import logging
|
||||
from datetime import datetime, timezone
|
||||
from typing import Any, Dict, List, Optional
|
||||
import time
|
||||
from datetime import datetime
|
||||
from typing import Any, Dict, Generator, List, Optional
|
||||
|
||||
import cv2
|
||||
from fastapi import APIRouter, Body, Depends, Request
|
||||
from fastapi.responses import JSONResponse
|
||||
from fastapi.responses import JSONResponse, StreamingResponse
|
||||
from pydantic import BaseModel
|
||||
|
||||
from frigate.api.auth import (
|
||||
@ -20,15 +21,60 @@ from frigate.api.defs.request.chat_body import ChatCompletionRequest
|
||||
from frigate.api.defs.response.chat_response import (
|
||||
ChatCompletionResponse,
|
||||
ChatMessageResponse,
|
||||
ToolCall,
|
||||
)
|
||||
from frigate.api.defs.tags import Tags
|
||||
from frigate.api.event import events
|
||||
from frigate.genai.utils import build_assistant_message_for_conversation
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
router = APIRouter(tags=[Tags.chat])
|
||||
|
||||
|
||||
def _chunk_content(content: str, chunk_size: int = 80) -> Generator[str, None, None]:
|
||||
"""Yield content in word-aware chunks for streaming."""
|
||||
if not content:
|
||||
return
|
||||
words = content.split(" ")
|
||||
current: List[str] = []
|
||||
current_len = 0
|
||||
for w in words:
|
||||
current.append(w)
|
||||
current_len += len(w) + 1
|
||||
if current_len >= chunk_size:
|
||||
yield " ".join(current) + " "
|
||||
current = []
|
||||
current_len = 0
|
||||
if current:
|
||||
yield " ".join(current)
|
||||
|
||||
|
||||
def _format_events_with_local_time(
|
||||
events_list: List[Dict[str, Any]],
|
||||
) -> List[Dict[str, Any]]:
|
||||
"""Add human-readable local start/end times to each event for the LLM."""
|
||||
result = []
|
||||
for evt in events_list:
|
||||
if not isinstance(evt, dict):
|
||||
result.append(evt)
|
||||
continue
|
||||
copy_evt = dict(evt)
|
||||
try:
|
||||
start_ts = evt.get("start_time")
|
||||
end_ts = evt.get("end_time")
|
||||
if start_ts is not None:
|
||||
dt_start = datetime.fromtimestamp(start_ts)
|
||||
copy_evt["start_time_local"] = dt_start.strftime("%Y-%m-%d %I:%M:%S %p")
|
||||
if end_ts is not None:
|
||||
dt_end = datetime.fromtimestamp(end_ts)
|
||||
copy_evt["end_time_local"] = dt_end.strftime("%Y-%m-%d %I:%M:%S %p")
|
||||
except (TypeError, ValueError, OSError):
|
||||
pass
|
||||
result.append(copy_evt)
|
||||
return result
|
||||
|
||||
|
||||
class ToolExecuteRequest(BaseModel):
|
||||
"""Request model for tool execution."""
|
||||
|
||||
@ -52,19 +98,25 @@ def get_tool_definitions() -> List[Dict[str, Any]]:
|
||||
"Search for detected objects in Frigate by camera, object label, time range, "
|
||||
"zones, and other filters. Use this to answer questions about when "
|
||||
"objects were detected, what objects appeared, or to find specific object detections. "
|
||||
"An 'object' in Frigate represents a tracked detection (e.g., a person, package, car)."
|
||||
"An 'object' in Frigate represents a tracked detection (e.g., a person, package, car). "
|
||||
"When the user asks about a specific name (person, delivery company, animal, etc.), "
|
||||
"filter by sub_label only and do not set label."
|
||||
),
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"camera": {
|
||||
"type": "string",
|
||||
"description": "Camera name to filter by (optional). Use 'all' for all cameras.",
|
||||
"description": "Camera name to filter by (optional).",
|
||||
},
|
||||
"label": {
|
||||
"type": "string",
|
||||
"description": "Object label to filter by (e.g., 'person', 'package', 'car').",
|
||||
},
|
||||
"sub_label": {
|
||||
"type": "string",
|
||||
"description": "Name of a person, delivery company, animal, etc. When filtering by a specific name, use only sub_label; do not set label.",
|
||||
},
|
||||
"after": {
|
||||
"type": "string",
|
||||
"description": "Start time in ISO 8601 format (e.g., '2024-01-01T00:00:00Z').",
|
||||
@ -80,8 +132,8 @@ def get_tool_definitions() -> List[Dict[str, Any]]:
|
||||
},
|
||||
"limit": {
|
||||
"type": "integer",
|
||||
"description": "Maximum number of objects to return (default: 10).",
|
||||
"default": 10,
|
||||
"description": "Maximum number of objects to return (default: 25).",
|
||||
"default": 25,
|
||||
},
|
||||
},
|
||||
},
|
||||
@ -119,14 +171,13 @@ def get_tool_definitions() -> List[Dict[str, Any]]:
|
||||
summary="Get available tools",
|
||||
description="Returns OpenAI-compatible tool definitions for function calling.",
|
||||
)
|
||||
def get_tools(request: Request) -> JSONResponse:
|
||||
def get_tools() -> JSONResponse:
|
||||
"""Get list of available tools for LLM function calling."""
|
||||
tools = get_tool_definitions()
|
||||
return JSONResponse(content={"tools": tools})
|
||||
|
||||
|
||||
async def _execute_search_objects(
|
||||
request: Request,
|
||||
arguments: Dict[str, Any],
|
||||
allowed_cameras: List[str],
|
||||
) -> JSONResponse:
|
||||
@ -136,23 +187,26 @@ async def _execute_search_objects(
|
||||
This searches for detected objects (events) in Frigate using the same
|
||||
logic as the events API endpoint.
|
||||
"""
|
||||
# Parse ISO 8601 timestamps to Unix timestamps if provided
|
||||
# Parse after/before as server local time; convert to Unix timestamp
|
||||
after = arguments.get("after")
|
||||
before = arguments.get("before")
|
||||
|
||||
def _parse_as_local_timestamp(s: str):
|
||||
s = s.replace("Z", "").strip()[:19]
|
||||
dt = datetime.strptime(s, "%Y-%m-%dT%H:%M:%S")
|
||||
return time.mktime(dt.timetuple())
|
||||
|
||||
if after:
|
||||
try:
|
||||
after_dt = datetime.fromisoformat(after.replace("Z", "+00:00"))
|
||||
after = after_dt.timestamp()
|
||||
except (ValueError, AttributeError):
|
||||
after = _parse_as_local_timestamp(after)
|
||||
except (ValueError, AttributeError, TypeError):
|
||||
logger.warning(f"Invalid 'after' timestamp format: {after}")
|
||||
after = None
|
||||
|
||||
if before:
|
||||
try:
|
||||
before_dt = datetime.fromisoformat(before.replace("Z", "+00:00"))
|
||||
before = before_dt.timestamp()
|
||||
except (ValueError, AttributeError):
|
||||
before = _parse_as_local_timestamp(before)
|
||||
except (ValueError, AttributeError, TypeError):
|
||||
logger.warning(f"Invalid 'before' timestamp format: {before}")
|
||||
before = None
|
||||
|
||||
@ -165,15 +219,14 @@ async def _execute_search_objects(
|
||||
|
||||
# Build query parameters compatible with EventsQueryParams
|
||||
query_params = EventsQueryParams(
|
||||
camera=arguments.get("camera", "all"),
|
||||
cameras=arguments.get("camera", "all"),
|
||||
label=arguments.get("label", "all"),
|
||||
labels=arguments.get("label", "all"),
|
||||
sub_labels=arguments.get("sub_label", "all").lower(),
|
||||
zones=zones,
|
||||
zone=zones,
|
||||
after=after,
|
||||
before=before,
|
||||
limit=arguments.get("limit", 10),
|
||||
limit=arguments.get("limit", 25),
|
||||
)
|
||||
|
||||
try:
|
||||
@ -202,7 +255,6 @@ async def _execute_search_objects(
|
||||
description="Execute a tool function call from an LLM.",
|
||||
)
|
||||
async def execute_tool(
|
||||
request: Request,
|
||||
body: ToolExecuteRequest = Body(...),
|
||||
allowed_cameras: List[str] = Depends(get_allowed_cameras_for_filter),
|
||||
) -> JSONResponse:
|
||||
@ -218,7 +270,7 @@ async def execute_tool(
|
||||
logger.debug(f"Executing tool: {tool_name} with arguments: {arguments}")
|
||||
|
||||
if tool_name == "search_objects":
|
||||
return await _execute_search_objects(request, arguments, allowed_cameras)
|
||||
return await _execute_search_objects(arguments, allowed_cameras)
|
||||
|
||||
return JSONResponse(
|
||||
content={
|
||||
@ -334,7 +386,7 @@ async def _execute_tool_internal(
|
||||
This is used by the chat completion endpoint to execute tools.
|
||||
"""
|
||||
if tool_name == "search_objects":
|
||||
response = await _execute_search_objects(request, arguments, allowed_cameras)
|
||||
response = await _execute_search_objects(arguments, allowed_cameras)
|
||||
try:
|
||||
if hasattr(response, "body"):
|
||||
body_str = response.body.decode("utf-8")
|
||||
@ -349,15 +401,109 @@ async def _execute_tool_internal(
|
||||
elif tool_name == "get_live_context":
|
||||
camera = arguments.get("camera")
|
||||
if not camera:
|
||||
logger.error(
|
||||
"Tool get_live_context failed: camera parameter is required. "
|
||||
"Arguments: %s",
|
||||
json.dumps(arguments),
|
||||
)
|
||||
return {"error": "Camera parameter is required"}
|
||||
return await _execute_get_live_context(request, camera, allowed_cameras)
|
||||
else:
|
||||
logger.error(
|
||||
"Tool call failed: unknown tool %r. Expected one of: search_objects, get_live_context. "
|
||||
"Arguments received: %s",
|
||||
tool_name,
|
||||
json.dumps(arguments),
|
||||
)
|
||||
return {"error": f"Unknown tool: {tool_name}"}
|
||||
|
||||
|
||||
async def _execute_pending_tools(
|
||||
pending_tool_calls: List[Dict[str, Any]],
|
||||
request: Request,
|
||||
allowed_cameras: List[str],
|
||||
) -> tuple[List[ToolCall], List[Dict[str, Any]]]:
|
||||
"""
|
||||
Execute a list of tool calls; return (ToolCall list for API response, tool result dicts for conversation).
|
||||
"""
|
||||
tool_calls_out: List[ToolCall] = []
|
||||
tool_results: List[Dict[str, Any]] = []
|
||||
for tool_call in pending_tool_calls:
|
||||
tool_name = tool_call["name"]
|
||||
tool_args = tool_call.get("arguments") or {}
|
||||
tool_call_id = tool_call["id"]
|
||||
logger.debug(
|
||||
f"Executing tool: {tool_name} (id: {tool_call_id}) with arguments: {json.dumps(tool_args, indent=2)}"
|
||||
)
|
||||
try:
|
||||
tool_result = await _execute_tool_internal(
|
||||
tool_name, tool_args, request, allowed_cameras
|
||||
)
|
||||
if isinstance(tool_result, dict) and tool_result.get("error"):
|
||||
logger.error(
|
||||
"Tool call %s (id: %s) returned error: %s. Arguments: %s",
|
||||
tool_name,
|
||||
tool_call_id,
|
||||
tool_result.get("error"),
|
||||
json.dumps(tool_args),
|
||||
)
|
||||
if tool_name == "search_objects" and isinstance(tool_result, list):
|
||||
tool_result = _format_events_with_local_time(tool_result)
|
||||
_keys = {
|
||||
"id",
|
||||
"camera",
|
||||
"label",
|
||||
"zones",
|
||||
"start_time_local",
|
||||
"end_time_local",
|
||||
"sub_label",
|
||||
"event_count",
|
||||
}
|
||||
tool_result = [
|
||||
{k: evt[k] for k in _keys if k in evt}
|
||||
for evt in tool_result
|
||||
if isinstance(evt, dict)
|
||||
]
|
||||
result_content = (
|
||||
json.dumps(tool_result)
|
||||
if isinstance(tool_result, (dict, list))
|
||||
else (tool_result if isinstance(tool_result, str) else str(tool_result))
|
||||
)
|
||||
tool_calls_out.append(
|
||||
ToolCall(name=tool_name, arguments=tool_args, response=result_content)
|
||||
)
|
||||
tool_results.append(
|
||||
{
|
||||
"role": "tool",
|
||||
"tool_call_id": tool_call_id,
|
||||
"content": result_content,
|
||||
}
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(
|
||||
"Error executing tool %s (id: %s): %s. Arguments: %s",
|
||||
tool_name,
|
||||
tool_call_id,
|
||||
e,
|
||||
json.dumps(tool_args),
|
||||
exc_info=True,
|
||||
)
|
||||
error_content = json.dumps({"error": f"Tool execution failed: {str(e)}"})
|
||||
tool_calls_out.append(
|
||||
ToolCall(name=tool_name, arguments=tool_args, response=error_content)
|
||||
)
|
||||
tool_results.append(
|
||||
{
|
||||
"role": "tool",
|
||||
"tool_call_id": tool_call_id,
|
||||
"content": error_content,
|
||||
}
|
||||
)
|
||||
return (tool_calls_out, tool_results)
|
||||
|
||||
|
||||
@router.post(
|
||||
"/chat/completion",
|
||||
response_model=ChatCompletionResponse,
|
||||
dependencies=[Depends(allow_any_authenticated())],
|
||||
summary="Chat completion with tool calling",
|
||||
description=(
|
||||
@ -369,7 +515,7 @@ async def chat_completion(
|
||||
request: Request,
|
||||
body: ChatCompletionRequest = Body(...),
|
||||
allowed_cameras: List[str] = Depends(get_allowed_cameras_for_filter),
|
||||
) -> JSONResponse:
|
||||
):
|
||||
"""
|
||||
Chat completion endpoint with tool calling support.
|
||||
|
||||
@ -394,9 +540,9 @@ async def chat_completion(
|
||||
tools = get_tool_definitions()
|
||||
conversation = []
|
||||
|
||||
current_datetime = datetime.now(timezone.utc)
|
||||
current_datetime = datetime.now()
|
||||
current_date_str = current_datetime.strftime("%Y-%m-%d")
|
||||
current_time_str = current_datetime.strftime("%H:%M:%S %Z")
|
||||
current_time_str = current_datetime.strftime("%I:%M:%S %p")
|
||||
|
||||
cameras_info = []
|
||||
config = request.app.frigate_config
|
||||
@ -429,9 +575,12 @@ async def chat_completion(
|
||||
|
||||
system_prompt = f"""You are a helpful assistant for Frigate, a security camera NVR system. You help users answer questions about their cameras, detected objects, and events.
|
||||
|
||||
Current date and time: {current_date_str} at {current_time_str} (UTC)
|
||||
Current server local date and time: {current_date_str} at {current_time_str}
|
||||
|
||||
When users ask questions about "today", "yesterday", "this week", etc., use the current date above as reference.
|
||||
Do not start your response with phrases like "I will check...", "Let me see...", or "Let me look...". Answer directly.
|
||||
|
||||
Always present times to the user in the server's local timezone. When tool results include start_time_local and end_time_local, use those exact strings when listing or describing detection times—do not convert or invent timestamps. Do not use UTC or ISO format with Z for the user-facing answer unless the tool result only provides Unix timestamps without local time fields.
|
||||
When users ask about "today", "yesterday", "this week", etc., use the current date above as reference.
|
||||
When searching for objects or events, use ISO 8601 format for dates (e.g., {current_date_str}T00:00:00Z for the start of today).
|
||||
Always be accurate with time calculations based on the current date provided.{cameras_section}{live_image_note}"""
|
||||
|
||||
@ -471,6 +620,7 @@ Always be accurate with time calculations based on the current date provided.{ca
|
||||
conversation.append(msg_dict)
|
||||
|
||||
tool_iterations = 0
|
||||
tool_calls: List[ToolCall] = []
|
||||
max_iterations = body.max_tool_iterations
|
||||
|
||||
logger.debug(
|
||||
@ -478,6 +628,81 @@ Always be accurate with time calculations based on the current date provided.{ca
|
||||
f"{len(tools)} tool(s) available, max_iterations={max_iterations}"
|
||||
)
|
||||
|
||||
# True LLM streaming when client supports it and stream requested
|
||||
if body.stream and hasattr(genai_client, "chat_with_tools_stream"):
|
||||
stream_tool_calls: List[ToolCall] = []
|
||||
stream_iterations = 0
|
||||
|
||||
async def stream_body_llm():
|
||||
nonlocal conversation, stream_tool_calls, stream_iterations
|
||||
while stream_iterations < max_iterations:
|
||||
logger.debug(
|
||||
f"Streaming LLM (iteration {stream_iterations + 1}/{max_iterations}) "
|
||||
f"with {len(conversation)} message(s)"
|
||||
)
|
||||
async for event in genai_client.chat_with_tools_stream(
|
||||
messages=conversation,
|
||||
tools=tools if tools else None,
|
||||
tool_choice="auto",
|
||||
):
|
||||
kind, value = event
|
||||
if kind == "content_delta":
|
||||
yield (
|
||||
json.dumps({"type": "content", "delta": value}).encode(
|
||||
"utf-8"
|
||||
)
|
||||
+ b"\n"
|
||||
)
|
||||
elif kind == "message":
|
||||
msg = value
|
||||
if msg.get("finish_reason") == "error":
|
||||
yield (
|
||||
json.dumps(
|
||||
{
|
||||
"type": "error",
|
||||
"error": "An error occurred while processing your request.",
|
||||
}
|
||||
).encode("utf-8")
|
||||
+ b"\n"
|
||||
)
|
||||
return
|
||||
pending = msg.get("tool_calls")
|
||||
if pending:
|
||||
stream_iterations += 1
|
||||
conversation.append(
|
||||
build_assistant_message_for_conversation(
|
||||
msg.get("content"), pending
|
||||
)
|
||||
)
|
||||
executed_calls, tool_results = await _execute_pending_tools(
|
||||
pending, request, allowed_cameras
|
||||
)
|
||||
stream_tool_calls.extend(executed_calls)
|
||||
conversation.extend(tool_results)
|
||||
yield (
|
||||
json.dumps(
|
||||
{
|
||||
"type": "tool_calls",
|
||||
"tool_calls": [
|
||||
tc.model_dump() for tc in stream_tool_calls
|
||||
],
|
||||
}
|
||||
).encode("utf-8")
|
||||
+ b"\n"
|
||||
)
|
||||
break
|
||||
else:
|
||||
yield (json.dumps({"type": "done"}).encode("utf-8") + b"\n")
|
||||
return
|
||||
else:
|
||||
yield json.dumps({"type": "done"}).encode("utf-8") + b"\n"
|
||||
|
||||
return StreamingResponse(
|
||||
stream_body_llm(),
|
||||
media_type="application/x-ndjson",
|
||||
headers={"X-Accel-Buffering": "no"},
|
||||
)
|
||||
|
||||
try:
|
||||
while tool_iterations < max_iterations:
|
||||
logger.debug(
|
||||
@ -499,119 +724,71 @@ Always be accurate with time calculations based on the current date provided.{ca
|
||||
status_code=500,
|
||||
)
|
||||
|
||||
assistant_message = {
|
||||
"role": "assistant",
|
||||
"content": response.get("content"),
|
||||
}
|
||||
if response.get("tool_calls"):
|
||||
assistant_message["tool_calls"] = [
|
||||
{
|
||||
"id": tc["id"],
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": tc["name"],
|
||||
"arguments": json.dumps(tc["arguments"]),
|
||||
},
|
||||
}
|
||||
for tc in response["tool_calls"]
|
||||
]
|
||||
conversation.append(assistant_message)
|
||||
conversation.append(
|
||||
build_assistant_message_for_conversation(
|
||||
response.get("content"), response.get("tool_calls")
|
||||
)
|
||||
)
|
||||
|
||||
tool_calls = response.get("tool_calls")
|
||||
if not tool_calls:
|
||||
pending_tool_calls = response.get("tool_calls")
|
||||
if not pending_tool_calls:
|
||||
logger.debug(
|
||||
f"Chat completion finished with final answer (iterations: {tool_iterations})"
|
||||
)
|
||||
final_content = response.get("content") or ""
|
||||
|
||||
if body.stream:
|
||||
|
||||
async def stream_body() -> Any:
|
||||
if tool_calls:
|
||||
yield (
|
||||
json.dumps(
|
||||
{
|
||||
"type": "tool_calls",
|
||||
"tool_calls": [
|
||||
tc.model_dump() for tc in tool_calls
|
||||
],
|
||||
}
|
||||
).encode("utf-8")
|
||||
+ b"\n"
|
||||
)
|
||||
# Stream content in word-sized chunks for smooth UX
|
||||
for part in _chunk_content(final_content):
|
||||
yield (
|
||||
json.dumps({"type": "content", "delta": part}).encode(
|
||||
"utf-8"
|
||||
)
|
||||
+ b"\n"
|
||||
)
|
||||
yield json.dumps({"type": "done"}).encode("utf-8") + b"\n"
|
||||
|
||||
return StreamingResponse(
|
||||
stream_body(),
|
||||
media_type="application/x-ndjson",
|
||||
)
|
||||
|
||||
return JSONResponse(
|
||||
content=ChatCompletionResponse(
|
||||
message=ChatMessageResponse(
|
||||
role="assistant",
|
||||
content=response.get("content"),
|
||||
content=final_content,
|
||||
tool_calls=None,
|
||||
),
|
||||
finish_reason=response.get("finish_reason", "stop"),
|
||||
tool_iterations=tool_iterations,
|
||||
tool_calls=tool_calls,
|
||||
).model_dump(),
|
||||
)
|
||||
|
||||
# Execute tools
|
||||
tool_iterations += 1
|
||||
logger.debug(
|
||||
f"Tool calls detected (iteration {tool_iterations}/{max_iterations}): "
|
||||
f"{len(tool_calls)} tool(s) to execute"
|
||||
f"{len(pending_tool_calls)} tool(s) to execute"
|
||||
)
|
||||
tool_results = []
|
||||
|
||||
for tool_call in tool_calls:
|
||||
tool_name = tool_call["name"]
|
||||
tool_args = tool_call["arguments"]
|
||||
tool_call_id = tool_call["id"]
|
||||
|
||||
logger.debug(
|
||||
f"Executing tool: {tool_name} (id: {tool_call_id}) with arguments: {json.dumps(tool_args, indent=2)}"
|
||||
)
|
||||
|
||||
try:
|
||||
tool_result = await _execute_tool_internal(
|
||||
tool_name, tool_args, request, allowed_cameras
|
||||
)
|
||||
|
||||
if isinstance(tool_result, dict):
|
||||
result_content = json.dumps(tool_result)
|
||||
result_summary = tool_result
|
||||
if isinstance(tool_result, dict) and isinstance(
|
||||
tool_result.get("content"), list
|
||||
):
|
||||
result_count = len(tool_result.get("content", []))
|
||||
result_summary = {
|
||||
"count": result_count,
|
||||
"sample": tool_result.get("content", [])[:2]
|
||||
if result_count > 0
|
||||
else [],
|
||||
}
|
||||
logger.debug(
|
||||
f"Tool {tool_name} (id: {tool_call_id}) completed successfully. "
|
||||
f"Result: {json.dumps(result_summary, indent=2)}"
|
||||
)
|
||||
elif isinstance(tool_result, str):
|
||||
result_content = tool_result
|
||||
logger.debug(
|
||||
f"Tool {tool_name} (id: {tool_call_id}) completed successfully. "
|
||||
f"Result length: {len(result_content)} characters"
|
||||
)
|
||||
else:
|
||||
result_content = str(tool_result)
|
||||
logger.debug(
|
||||
f"Tool {tool_name} (id: {tool_call_id}) completed successfully. "
|
||||
f"Result type: {type(tool_result).__name__}"
|
||||
)
|
||||
|
||||
tool_results.append(
|
||||
{
|
||||
"role": "tool",
|
||||
"tool_call_id": tool_call_id,
|
||||
"content": result_content,
|
||||
}
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(
|
||||
f"Error executing tool {tool_name} (id: {tool_call_id}): {e}",
|
||||
exc_info=True,
|
||||
)
|
||||
error_content = json.dumps(
|
||||
{"error": f"Tool execution failed: {str(e)}"}
|
||||
)
|
||||
tool_results.append(
|
||||
{
|
||||
"role": "tool",
|
||||
"tool_call_id": tool_call_id,
|
||||
"content": error_content,
|
||||
}
|
||||
)
|
||||
logger.debug(
|
||||
f"Tool {tool_name} (id: {tool_call_id}) failed. Error result added to conversation."
|
||||
)
|
||||
|
||||
executed_calls, tool_results = await _execute_pending_tools(
|
||||
pending_tool_calls, request, allowed_cameras
|
||||
)
|
||||
tool_calls.extend(executed_calls)
|
||||
conversation.extend(tool_results)
|
||||
logger.debug(
|
||||
f"Added {len(tool_results)} tool result(s) to conversation. "
|
||||
@ -630,6 +807,7 @@ Always be accurate with time calculations based on the current date provided.{ca
|
||||
),
|
||||
finish_reason="length",
|
||||
tool_iterations=tool_iterations,
|
||||
tool_calls=tool_calls,
|
||||
).model_dump(),
|
||||
)
|
||||
|
||||
|
||||
@ -39,3 +39,7 @@ class ChatCompletionRequest(BaseModel):
|
||||
"user message as multimodal content. Use with get_live_context for detection info."
|
||||
),
|
||||
)
|
||||
stream: bool = Field(
|
||||
default=False,
|
||||
description="If true, stream the final assistant response in the body as newline-delimited JSON.",
|
||||
)
|
||||
|
||||
@ -5,8 +5,8 @@ from typing import Any, Optional
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
|
||||
class ToolCall(BaseModel):
|
||||
"""A tool call from the LLM."""
|
||||
class ToolCallInvocation(BaseModel):
|
||||
"""A tool call requested by the LLM (before execution)."""
|
||||
|
||||
id: str = Field(description="Unique identifier for this tool call")
|
||||
name: str = Field(description="Tool name to call")
|
||||
@ -20,11 +20,24 @@ class ChatMessageResponse(BaseModel):
|
||||
content: Optional[str] = Field(
|
||||
default=None, description="Message content (None if tool calls present)"
|
||||
)
|
||||
tool_calls: Optional[list[ToolCall]] = Field(
|
||||
tool_calls: Optional[list[ToolCallInvocation]] = Field(
|
||||
default=None, description="Tool calls if LLM wants to call tools"
|
||||
)
|
||||
|
||||
|
||||
class ToolCall(BaseModel):
|
||||
"""A tool that was executed during the completion, with its response."""
|
||||
|
||||
name: str = Field(description="Tool name that was called")
|
||||
arguments: dict[str, Any] = Field(
|
||||
default_factory=dict, description="Arguments passed to the tool"
|
||||
)
|
||||
response: str = Field(
|
||||
default="",
|
||||
description="The response or result returned from the tool execution",
|
||||
)
|
||||
|
||||
|
||||
class ChatCompletionResponse(BaseModel):
|
||||
"""Response from chat completion."""
|
||||
|
||||
@ -35,3 +48,7 @@ class ChatCompletionResponse(BaseModel):
|
||||
tool_iterations: int = Field(
|
||||
default=0, description="Number of tool call iterations performed"
|
||||
)
|
||||
tool_calls: list[ToolCall] = Field(
|
||||
default_factory=list,
|
||||
description="List of tool calls that were executed during this completion",
|
||||
)
|
||||
|
||||
@ -737,6 +737,7 @@ async def event_snapshot(
|
||||
):
|
||||
event_complete = False
|
||||
jpg_bytes = None
|
||||
frame_time = 0
|
||||
try:
|
||||
event = Event.get(Event.id == event_id, Event.end_time != None)
|
||||
event_complete = True
|
||||
@ -790,7 +791,7 @@ async def event_snapshot(
|
||||
headers = {
|
||||
"Content-Type": "image/jpeg",
|
||||
"Cache-Control": "private, max-age=31536000" if event_complete else "no-store",
|
||||
"X-Frame-Time": frame_time,
|
||||
"X-Frame-Time": str(frame_time),
|
||||
}
|
||||
|
||||
if params.download:
|
||||
|
||||
@ -1,6 +1,5 @@
|
||||
"""Gemini Provider for Frigate AI."""
|
||||
|
||||
import json
|
||||
import logging
|
||||
from typing import Any, Optional
|
||||
|
||||
@ -84,147 +83,169 @@ class GeminiClient(GenAIClient):
|
||||
tools: Optional[list[dict[str, Any]]] = None,
|
||||
tool_choice: Optional[str] = "auto",
|
||||
) -> dict[str, Any]:
|
||||
"""
|
||||
Send chat messages to Gemini with optional tool definitions.
|
||||
|
||||
Implements function calling/tool usage for Gemini models.
|
||||
"""
|
||||
try:
|
||||
# Convert messages to Gemini format
|
||||
gemini_messages = []
|
||||
for msg in messages:
|
||||
role = msg.get("role", "user")
|
||||
content = msg.get("content", "")
|
||||
|
||||
# Map roles to Gemini format
|
||||
if role == "system":
|
||||
# Gemini doesn't have system role, prepend to first user message
|
||||
if gemini_messages and gemini_messages[0].role == "user":
|
||||
gemini_messages[0].parts[
|
||||
0
|
||||
].text = f"{content}\n\n{gemini_messages[0].parts[0].text}"
|
||||
else:
|
||||
gemini_messages.append(
|
||||
types.Content(
|
||||
role="user", parts=[types.Part.from_text(text=content)]
|
||||
)
|
||||
)
|
||||
elif role == "assistant":
|
||||
gemini_messages.append(
|
||||
types.Content(
|
||||
role="model", parts=[types.Part.from_text(text=content)]
|
||||
)
|
||||
)
|
||||
elif role == "tool":
|
||||
# Handle tool response
|
||||
function_response = {
|
||||
"name": msg.get("name", ""),
|
||||
"response": content,
|
||||
}
|
||||
gemini_messages.append(
|
||||
types.Content(
|
||||
role="function",
|
||||
parts=[
|
||||
types.Part.from_function_response(function_response)
|
||||
],
|
||||
)
|
||||
)
|
||||
else: # user
|
||||
gemini_messages.append(
|
||||
types.Content(
|
||||
role="user", parts=[types.Part.from_text(text=content)]
|
||||
)
|
||||
)
|
||||
|
||||
# Convert tools to Gemini format
|
||||
gemini_tools = None
|
||||
if tools:
|
||||
function_declarations = []
|
||||
gemini_tools = []
|
||||
for tool in tools:
|
||||
if tool.get("type") == "function":
|
||||
func_def = tool.get("function", {})
|
||||
function_declarations.append(
|
||||
genai.protos.FunctionDeclaration(
|
||||
name=func_def.get("name"),
|
||||
description=func_def.get("description"),
|
||||
parameters=genai.protos.Schema(
|
||||
type=genai.protos.Type.OBJECT,
|
||||
properties={
|
||||
prop_name: genai.protos.Schema(
|
||||
type=_convert_json_type_to_gemini(
|
||||
prop.get("type")
|
||||
),
|
||||
description=prop.get("description"),
|
||||
)
|
||||
for prop_name, prop in func_def.get(
|
||||
"parameters", {}
|
||||
)
|
||||
.get("properties", {})
|
||||
.items()
|
||||
},
|
||||
required=func_def.get("parameters", {}).get(
|
||||
"required", []
|
||||
),
|
||||
),
|
||||
func = tool.get("function", {})
|
||||
gemini_tools.append(
|
||||
types.Tool(
|
||||
function_declarations=[
|
||||
types.FunctionDeclaration(
|
||||
name=func.get("name", ""),
|
||||
description=func.get("description", ""),
|
||||
parameters=func.get("parameters", {}),
|
||||
)
|
||||
]
|
||||
)
|
||||
)
|
||||
|
||||
tool_config = genai.protos.Tool(
|
||||
function_declarations=function_declarations
|
||||
)
|
||||
|
||||
# Configure tool choice
|
||||
tool_config = None
|
||||
if tool_choice:
|
||||
if tool_choice == "none":
|
||||
function_calling_config = genai.protos.FunctionCallingConfig(
|
||||
mode=genai.protos.FunctionCallingConfig.Mode.NONE
|
||||
tool_config = types.ToolConfig(
|
||||
function_calling_config=types.FunctionCallingConfig(mode="NONE")
|
||||
)
|
||||
elif tool_choice == "auto":
|
||||
tool_config = types.ToolConfig(
|
||||
function_calling_config=types.FunctionCallingConfig(mode="AUTO")
|
||||
)
|
||||
elif tool_choice == "required":
|
||||
function_calling_config = genai.protos.FunctionCallingConfig(
|
||||
mode=genai.protos.FunctionCallingConfig.Mode.ANY
|
||||
)
|
||||
else:
|
||||
function_calling_config = genai.protos.FunctionCallingConfig(
|
||||
mode=genai.protos.FunctionCallingConfig.Mode.AUTO
|
||||
)
|
||||
else:
|
||||
tool_config = None
|
||||
function_calling_config = None
|
||||
|
||||
contents = []
|
||||
for msg in messages:
|
||||
role = msg.get("role")
|
||||
content = msg.get("content", "")
|
||||
|
||||
if role == "system":
|
||||
continue
|
||||
elif role == "user":
|
||||
contents.append({"role": "user", "parts": [content]})
|
||||
elif role == "assistant":
|
||||
parts = [content] if content else []
|
||||
if "tool_calls" in msg:
|
||||
for tc in msg["tool_calls"]:
|
||||
parts.append(
|
||||
genai.protos.FunctionCall(
|
||||
name=tc["function"]["name"],
|
||||
args=json.loads(tc["function"]["arguments"]),
|
||||
)
|
||||
)
|
||||
contents.append({"role": "model", "parts": parts})
|
||||
elif role == "tool":
|
||||
tool_name = msg.get("name", "")
|
||||
tool_result = (
|
||||
json.loads(content) if isinstance(content, str) else content
|
||||
)
|
||||
contents.append(
|
||||
{
|
||||
"role": "function",
|
||||
"parts": [
|
||||
genai.protos.FunctionResponse(
|
||||
name=tool_name,
|
||||
response=tool_result,
|
||||
)
|
||||
],
|
||||
}
|
||||
tool_config = types.ToolConfig(
|
||||
function_calling_config=types.FunctionCallingConfig(mode="ANY")
|
||||
)
|
||||
|
||||
generation_config = genai.types.GenerationConfig(
|
||||
candidate_count=1,
|
||||
)
|
||||
if function_calling_config:
|
||||
generation_config.function_calling_config = function_calling_config
|
||||
# Build request config
|
||||
config_params = {"candidate_count": 1}
|
||||
|
||||
response = self.provider.generate_content(
|
||||
contents,
|
||||
tools=[tool_config] if tool_config else None,
|
||||
generation_config=generation_config,
|
||||
request_options=genai.types.RequestOptions(timeout=self.timeout),
|
||||
if gemini_tools:
|
||||
config_params["tools"] = gemini_tools
|
||||
|
||||
if tool_config:
|
||||
config_params["tool_config"] = tool_config
|
||||
|
||||
# Merge runtime_options
|
||||
if isinstance(self.genai_config.runtime_options, dict):
|
||||
config_params.update(self.genai_config.runtime_options)
|
||||
|
||||
response = self.provider.models.generate_content(
|
||||
model=self.genai_config.model,
|
||||
contents=gemini_messages,
|
||||
config=types.GenerateContentConfig(**config_params),
|
||||
)
|
||||
|
||||
# Check if response is valid
|
||||
if not response or not response.candidates:
|
||||
return {
|
||||
"content": None,
|
||||
"tool_calls": None,
|
||||
"finish_reason": "error",
|
||||
}
|
||||
|
||||
candidate = response.candidates[0]
|
||||
content = None
|
||||
tool_calls = None
|
||||
|
||||
if response.candidates and response.candidates[0].content:
|
||||
parts = response.candidates[0].content.parts
|
||||
text_parts = [p.text for p in parts if hasattr(p, "text") and p.text]
|
||||
if text_parts:
|
||||
content = " ".join(text_parts).strip()
|
||||
# Extract content and tool calls from response
|
||||
if candidate.content and candidate.content.parts:
|
||||
for part in candidate.content.parts:
|
||||
if part.text:
|
||||
content = part.text.strip()
|
||||
elif part.function_call:
|
||||
# Handle function call
|
||||
if tool_calls is None:
|
||||
tool_calls = []
|
||||
|
||||
try:
|
||||
arguments = (
|
||||
dict(part.function_call.args)
|
||||
if part.function_call.args
|
||||
else {}
|
||||
)
|
||||
except Exception:
|
||||
arguments = {}
|
||||
|
||||
function_calls = [
|
||||
p.function_call
|
||||
for p in parts
|
||||
if hasattr(p, "function_call") and p.function_call
|
||||
]
|
||||
if function_calls:
|
||||
tool_calls = []
|
||||
for fc in function_calls:
|
||||
tool_calls.append(
|
||||
{
|
||||
"id": f"call_{hash(fc.name)}",
|
||||
"name": fc.name,
|
||||
"arguments": dict(fc.args)
|
||||
if hasattr(fc, "args")
|
||||
else {},
|
||||
"id": part.function_call.name or "",
|
||||
"name": part.function_call.name or "",
|
||||
"arguments": arguments,
|
||||
}
|
||||
)
|
||||
|
||||
# Determine finish reason
|
||||
finish_reason = "error"
|
||||
if response.candidates:
|
||||
finish_reason_map = {
|
||||
genai.types.FinishReason.STOP: "stop",
|
||||
genai.types.FinishReason.MAX_TOKENS: "length",
|
||||
genai.types.FinishReason.SAFETY: "stop",
|
||||
genai.types.FinishReason.RECITATION: "stop",
|
||||
genai.types.FinishReason.OTHER: "error",
|
||||
}
|
||||
finish_reason = finish_reason_map.get(
|
||||
response.candidates[0].finish_reason, "error"
|
||||
)
|
||||
if hasattr(candidate, "finish_reason") and candidate.finish_reason:
|
||||
from google.genai.types import FinishReason
|
||||
|
||||
if candidate.finish_reason == FinishReason.STOP:
|
||||
finish_reason = "stop"
|
||||
elif candidate.finish_reason == FinishReason.MAX_TOKENS:
|
||||
finish_reason = "length"
|
||||
elif candidate.finish_reason in [
|
||||
FinishReason.SAFETY,
|
||||
FinishReason.RECITATION,
|
||||
]:
|
||||
finish_reason = "error"
|
||||
elif tool_calls:
|
||||
finish_reason = "tool_calls"
|
||||
elif content:
|
||||
finish_reason = "stop"
|
||||
elif tool_calls:
|
||||
finish_reason = "tool_calls"
|
||||
elif content:
|
||||
@ -236,29 +257,19 @@ class GeminiClient(GenAIClient):
|
||||
"finish_reason": finish_reason,
|
||||
}
|
||||
|
||||
except GoogleAPICallError as e:
|
||||
logger.warning("Gemini returned an error: %s", str(e))
|
||||
except errors.APIError as e:
|
||||
logger.warning("Gemini API error during chat_with_tools: %s", str(e))
|
||||
return {
|
||||
"content": None,
|
||||
"tool_calls": None,
|
||||
"finish_reason": "error",
|
||||
}
|
||||
except Exception as e:
|
||||
logger.warning("Unexpected error in Gemini chat_with_tools: %s", str(e))
|
||||
logger.warning(
|
||||
"Gemini returned an error during chat_with_tools: %s", str(e)
|
||||
)
|
||||
return {
|
||||
"content": None,
|
||||
"tool_calls": None,
|
||||
"finish_reason": "error",
|
||||
}
|
||||
|
||||
|
||||
def _convert_json_type_to_gemini(json_type: str) -> genai.protos.Type:
|
||||
type_map = {
|
||||
"string": genai.protos.Type.STRING,
|
||||
"integer": genai.protos.Type.INTEGER,
|
||||
"number": genai.protos.Type.NUMBER,
|
||||
"boolean": genai.protos.Type.BOOLEAN,
|
||||
"array": genai.protos.Type.ARRAY,
|
||||
"object": genai.protos.Type.OBJECT,
|
||||
}
|
||||
return type_map.get(json_type, genai.protos.Type.STRING)
|
||||
|
||||
@ -6,12 +6,14 @@ import json
|
||||
import logging
|
||||
from typing import Any, Optional
|
||||
|
||||
import httpx
|
||||
import numpy as np
|
||||
import requests
|
||||
from PIL import Image
|
||||
|
||||
from frigate.config import GenAIProviderEnum
|
||||
from frigate.genai import GenAIClient, register_genai_provider
|
||||
from frigate.genai.utils import parse_tool_calls_from_message
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@ -84,6 +86,7 @@ class LlamaCppClient(GenAIClient):
|
||||
|
||||
# Build request payload with llama.cpp native options
|
||||
payload = {
|
||||
"model": self.genai_config.model,
|
||||
"messages": [
|
||||
{
|
||||
"role": "user",
|
||||
@ -116,7 +119,79 @@ class LlamaCppClient(GenAIClient):
|
||||
|
||||
def get_context_size(self) -> int:
|
||||
"""Get the context window size for llama.cpp."""
|
||||
return self.genai_config.provider_options.get("context_size", 4096)
|
||||
return self.provider_options.get("context_size", 4096)
|
||||
|
||||
def _build_payload(
|
||||
self,
|
||||
messages: list[dict[str, Any]],
|
||||
tools: Optional[list[dict[str, Any]]],
|
||||
tool_choice: Optional[str],
|
||||
stream: bool = False,
|
||||
) -> dict[str, Any]:
|
||||
"""Build request payload for chat completions (sync or stream)."""
|
||||
openai_tool_choice = None
|
||||
if tool_choice:
|
||||
if tool_choice == "none":
|
||||
openai_tool_choice = "none"
|
||||
elif tool_choice == "auto":
|
||||
openai_tool_choice = "auto"
|
||||
elif tool_choice == "required":
|
||||
openai_tool_choice = "required"
|
||||
|
||||
payload: dict[str, Any] = {
|
||||
"messages": messages,
|
||||
"model": self.genai_config.model,
|
||||
}
|
||||
if stream:
|
||||
payload["stream"] = True
|
||||
if tools:
|
||||
payload["tools"] = tools
|
||||
if openai_tool_choice is not None:
|
||||
payload["tool_choice"] = openai_tool_choice
|
||||
provider_opts = {
|
||||
k: v for k, v in self.provider_options.items() if k != "context_size"
|
||||
}
|
||||
payload.update(provider_opts)
|
||||
return payload
|
||||
|
||||
def _message_from_choice(self, choice: dict[str, Any]) -> dict[str, Any]:
|
||||
"""Parse OpenAI-style choice into {content, tool_calls, finish_reason}."""
|
||||
message = choice.get("message", {})
|
||||
content = message.get("content")
|
||||
content = content.strip() if content else None
|
||||
tool_calls = parse_tool_calls_from_message(message)
|
||||
finish_reason = choice.get("finish_reason") or (
|
||||
"tool_calls" if tool_calls else "stop" if content else "error"
|
||||
)
|
||||
return {
|
||||
"content": content,
|
||||
"tool_calls": tool_calls,
|
||||
"finish_reason": finish_reason,
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def _streamed_tool_calls_to_list(
|
||||
tool_calls_by_index: dict[int, dict[str, Any]],
|
||||
) -> Optional[list[dict[str, Any]]]:
|
||||
"""Convert streamed tool_calls index map to list of {id, name, arguments}."""
|
||||
if not tool_calls_by_index:
|
||||
return None
|
||||
result = []
|
||||
for idx in sorted(tool_calls_by_index.keys()):
|
||||
t = tool_calls_by_index[idx]
|
||||
args_str = t.get("arguments") or "{}"
|
||||
try:
|
||||
arguments = json.loads(args_str)
|
||||
except json.JSONDecodeError:
|
||||
arguments = {}
|
||||
result.append(
|
||||
{
|
||||
"id": t.get("id", ""),
|
||||
"name": t.get("name", ""),
|
||||
"arguments": arguments,
|
||||
}
|
||||
)
|
||||
return result if result else None
|
||||
|
||||
def embed(
|
||||
self,
|
||||
@ -151,15 +226,17 @@ class LlamaCppClient(GenAIClient):
|
||||
to_encode = jpeg_bytes if jpeg_bytes is not None else img
|
||||
encoded = base64.b64encode(to_encode).decode("utf-8")
|
||||
# prompt_string must contain <__media__> placeholder for image tokenization
|
||||
content.append({
|
||||
"prompt_string": "<__media__>\n",
|
||||
"multimodal_data": [encoded],
|
||||
})
|
||||
content.append(
|
||||
{
|
||||
"prompt_string": "<__media__>\n",
|
||||
"multimodal_data": [encoded],
|
||||
}
|
||||
)
|
||||
|
||||
try:
|
||||
response = requests.post(
|
||||
f"{self.provider}/embeddings",
|
||||
json={"content": content},
|
||||
json={"model": self.genai_config.model, "content": content},
|
||||
timeout=self.timeout,
|
||||
)
|
||||
response.raise_for_status()
|
||||
@ -237,31 +314,8 @@ class LlamaCppClient(GenAIClient):
|
||||
"tool_calls": None,
|
||||
"finish_reason": "error",
|
||||
}
|
||||
|
||||
try:
|
||||
openai_tool_choice = None
|
||||
if tool_choice:
|
||||
if tool_choice == "none":
|
||||
openai_tool_choice = "none"
|
||||
elif tool_choice == "auto":
|
||||
openai_tool_choice = "auto"
|
||||
elif tool_choice == "required":
|
||||
openai_tool_choice = "required"
|
||||
|
||||
payload = {
|
||||
"messages": messages,
|
||||
}
|
||||
|
||||
if tools:
|
||||
payload["tools"] = tools
|
||||
if openai_tool_choice is not None:
|
||||
payload["tool_choice"] = openai_tool_choice
|
||||
|
||||
provider_opts = {
|
||||
k: v for k, v in self.provider_options.items() if k != "context_size"
|
||||
}
|
||||
payload.update(provider_opts)
|
||||
|
||||
payload = self._build_payload(messages, tools, tool_choice, stream=False)
|
||||
response = requests.post(
|
||||
f"{self.provider}/v1/chat/completions",
|
||||
json=payload,
|
||||
@ -269,60 +323,13 @@ class LlamaCppClient(GenAIClient):
|
||||
)
|
||||
response.raise_for_status()
|
||||
result = response.json()
|
||||
|
||||
if result is None or "choices" not in result or len(result["choices"]) == 0:
|
||||
return {
|
||||
"content": None,
|
||||
"tool_calls": None,
|
||||
"finish_reason": "error",
|
||||
}
|
||||
|
||||
choice = result["choices"][0]
|
||||
message = choice.get("message", {})
|
||||
|
||||
content = message.get("content")
|
||||
if content:
|
||||
content = content.strip()
|
||||
else:
|
||||
content = None
|
||||
|
||||
tool_calls = None
|
||||
if "tool_calls" in message and message["tool_calls"]:
|
||||
tool_calls = []
|
||||
for tool_call in message["tool_calls"]:
|
||||
try:
|
||||
function_data = tool_call.get("function", {})
|
||||
arguments_str = function_data.get("arguments", "{}")
|
||||
arguments = json.loads(arguments_str)
|
||||
except (json.JSONDecodeError, KeyError, TypeError) as e:
|
||||
logger.warning(
|
||||
f"Failed to parse tool call arguments: {e}, "
|
||||
f"tool: {function_data.get('name', 'unknown')}"
|
||||
)
|
||||
arguments = {}
|
||||
|
||||
tool_calls.append(
|
||||
{
|
||||
"id": tool_call.get("id", ""),
|
||||
"name": function_data.get("name", ""),
|
||||
"arguments": arguments,
|
||||
}
|
||||
)
|
||||
|
||||
finish_reason = "error"
|
||||
if "finish_reason" in choice and choice["finish_reason"]:
|
||||
finish_reason = choice["finish_reason"]
|
||||
elif tool_calls:
|
||||
finish_reason = "tool_calls"
|
||||
elif content:
|
||||
finish_reason = "stop"
|
||||
|
||||
return {
|
||||
"content": content,
|
||||
"tool_calls": tool_calls,
|
||||
"finish_reason": finish_reason,
|
||||
}
|
||||
|
||||
return self._message_from_choice(result["choices"][0])
|
||||
except requests.exceptions.Timeout as e:
|
||||
logger.warning("llama.cpp request timed out: %s", str(e))
|
||||
return {
|
||||
@ -334,8 +341,7 @@ class LlamaCppClient(GenAIClient):
|
||||
error_detail = str(e)
|
||||
if hasattr(e, "response") and e.response is not None:
|
||||
try:
|
||||
error_body = e.response.text
|
||||
error_detail = f"{str(e)} - Response: {error_body[:500]}"
|
||||
error_detail = f"{str(e)} - Response: {e.response.text[:500]}"
|
||||
except Exception:
|
||||
pass
|
||||
logger.warning("llama.cpp returned an error: %s", error_detail)
|
||||
@ -351,3 +357,111 @@ class LlamaCppClient(GenAIClient):
|
||||
"tool_calls": None,
|
||||
"finish_reason": "error",
|
||||
}
|
||||
|
||||
async def chat_with_tools_stream(
|
||||
self,
|
||||
messages: list[dict[str, Any]],
|
||||
tools: Optional[list[dict[str, Any]]] = None,
|
||||
tool_choice: Optional[str] = "auto",
|
||||
):
|
||||
"""Stream chat with tools via OpenAI-compatible streaming API."""
|
||||
if self.provider is None:
|
||||
logger.warning(
|
||||
"llama.cpp provider has not been initialized. Check your llama.cpp configuration."
|
||||
)
|
||||
yield (
|
||||
"message",
|
||||
{
|
||||
"content": None,
|
||||
"tool_calls": None,
|
||||
"finish_reason": "error",
|
||||
},
|
||||
)
|
||||
return
|
||||
try:
|
||||
payload = self._build_payload(messages, tools, tool_choice, stream=True)
|
||||
content_parts: list[str] = []
|
||||
tool_calls_by_index: dict[int, dict[str, Any]] = {}
|
||||
finish_reason = "stop"
|
||||
|
||||
async with httpx.AsyncClient(timeout=float(self.timeout)) as client:
|
||||
async with client.stream(
|
||||
"POST",
|
||||
f"{self.provider}/v1/chat/completions",
|
||||
json=payload,
|
||||
) as response:
|
||||
response.raise_for_status()
|
||||
async for line in response.aiter_lines():
|
||||
if not line.startswith("data: "):
|
||||
continue
|
||||
data_str = line[6:].strip()
|
||||
if data_str == "[DONE]":
|
||||
break
|
||||
try:
|
||||
data = json.loads(data_str)
|
||||
except json.JSONDecodeError:
|
||||
continue
|
||||
choices = data.get("choices") or []
|
||||
if not choices:
|
||||
continue
|
||||
delta = choices[0].get("delta", {})
|
||||
if choices[0].get("finish_reason"):
|
||||
finish_reason = choices[0]["finish_reason"]
|
||||
if delta.get("content"):
|
||||
content_parts.append(delta["content"])
|
||||
yield ("content_delta", delta["content"])
|
||||
for tc in delta.get("tool_calls") or []:
|
||||
idx = tc.get("index", 0)
|
||||
fn = tc.get("function") or {}
|
||||
if idx not in tool_calls_by_index:
|
||||
tool_calls_by_index[idx] = {
|
||||
"id": tc.get("id", ""),
|
||||
"name": tc.get("name") or fn.get("name", ""),
|
||||
"arguments": "",
|
||||
}
|
||||
t = tool_calls_by_index[idx]
|
||||
if tc.get("id"):
|
||||
t["id"] = tc["id"]
|
||||
name = tc.get("name") or fn.get("name")
|
||||
if name:
|
||||
t["name"] = name
|
||||
arg = tc.get("arguments") or fn.get("arguments")
|
||||
if arg is not None:
|
||||
t["arguments"] += (
|
||||
arg if isinstance(arg, str) else json.dumps(arg)
|
||||
)
|
||||
|
||||
full_content = "".join(content_parts).strip() or None
|
||||
tool_calls_list = self._streamed_tool_calls_to_list(tool_calls_by_index)
|
||||
if tool_calls_list:
|
||||
finish_reason = "tool_calls"
|
||||
yield (
|
||||
"message",
|
||||
{
|
||||
"content": full_content,
|
||||
"tool_calls": tool_calls_list,
|
||||
"finish_reason": finish_reason,
|
||||
},
|
||||
)
|
||||
except httpx.HTTPStatusError as e:
|
||||
logger.warning("llama.cpp streaming HTTP error: %s", e)
|
||||
yield (
|
||||
"message",
|
||||
{
|
||||
"content": None,
|
||||
"tool_calls": None,
|
||||
"finish_reason": "error",
|
||||
},
|
||||
)
|
||||
except Exception as e:
|
||||
logger.warning(
|
||||
"Unexpected error in llama.cpp chat_with_tools_stream: %s", str(e)
|
||||
)
|
||||
yield (
|
||||
"message",
|
||||
{
|
||||
"content": None,
|
||||
"tool_calls": None,
|
||||
"finish_reason": "error",
|
||||
},
|
||||
)
|
||||
|
||||
@ -1,15 +1,16 @@
|
||||
"""Ollama Provider for Frigate AI."""
|
||||
|
||||
import json
|
||||
import logging
|
||||
from typing import Any, Optional
|
||||
|
||||
from httpx import RemoteProtocolError, TimeoutException
|
||||
from ollama import AsyncClient as OllamaAsyncClient
|
||||
from ollama import Client as ApiClient
|
||||
from ollama import ResponseError
|
||||
|
||||
from frigate.config import GenAIProviderEnum
|
||||
from frigate.genai import GenAIClient, register_genai_provider
|
||||
from frigate.genai.utils import parse_tool_calls_from_message
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@ -88,6 +89,73 @@ class OllamaClient(GenAIClient):
|
||||
"num_ctx", 4096
|
||||
)
|
||||
|
||||
def _build_request_params(
|
||||
self,
|
||||
messages: list[dict[str, Any]],
|
||||
tools: Optional[list[dict[str, Any]]],
|
||||
tool_choice: Optional[str],
|
||||
stream: bool = False,
|
||||
) -> dict[str, Any]:
|
||||
"""Build request_messages and params for chat (sync or stream)."""
|
||||
request_messages = []
|
||||
for msg in messages:
|
||||
msg_dict = {
|
||||
"role": msg.get("role"),
|
||||
"content": msg.get("content", ""),
|
||||
}
|
||||
if msg.get("tool_call_id"):
|
||||
msg_dict["tool_call_id"] = msg["tool_call_id"]
|
||||
if msg.get("name"):
|
||||
msg_dict["name"] = msg["name"]
|
||||
if msg.get("tool_calls"):
|
||||
msg_dict["tool_calls"] = msg["tool_calls"]
|
||||
request_messages.append(msg_dict)
|
||||
|
||||
request_params: dict[str, Any] = {
|
||||
"model": self.genai_config.model,
|
||||
"messages": request_messages,
|
||||
**self.provider_options,
|
||||
}
|
||||
if stream:
|
||||
request_params["stream"] = True
|
||||
if tools:
|
||||
request_params["tools"] = tools
|
||||
if tool_choice:
|
||||
request_params["tool_choice"] = (
|
||||
"none"
|
||||
if tool_choice == "none"
|
||||
else "required"
|
||||
if tool_choice == "required"
|
||||
else "auto"
|
||||
)
|
||||
return request_params
|
||||
|
||||
def _message_from_response(self, response: dict[str, Any]) -> dict[str, Any]:
|
||||
"""Parse Ollama chat response into {content, tool_calls, finish_reason}."""
|
||||
if not response or "message" not in response:
|
||||
return {
|
||||
"content": None,
|
||||
"tool_calls": None,
|
||||
"finish_reason": "error",
|
||||
}
|
||||
message = response["message"]
|
||||
content = message.get("content", "").strip() if message.get("content") else None
|
||||
tool_calls = parse_tool_calls_from_message(message)
|
||||
finish_reason = "error"
|
||||
if response.get("done"):
|
||||
finish_reason = (
|
||||
"tool_calls" if tool_calls else "stop" if content else "error"
|
||||
)
|
||||
elif tool_calls:
|
||||
finish_reason = "tool_calls"
|
||||
elif content:
|
||||
finish_reason = "stop"
|
||||
return {
|
||||
"content": content,
|
||||
"tool_calls": tool_calls,
|
||||
"finish_reason": finish_reason,
|
||||
}
|
||||
|
||||
def chat_with_tools(
|
||||
self,
|
||||
messages: list[dict[str, Any]],
|
||||
@ -103,93 +171,12 @@ class OllamaClient(GenAIClient):
|
||||
"tool_calls": None,
|
||||
"finish_reason": "error",
|
||||
}
|
||||
|
||||
try:
|
||||
request_messages = []
|
||||
for msg in messages:
|
||||
msg_dict = {
|
||||
"role": msg.get("role"),
|
||||
"content": msg.get("content", ""),
|
||||
}
|
||||
if msg.get("tool_call_id"):
|
||||
msg_dict["tool_call_id"] = msg["tool_call_id"]
|
||||
if msg.get("name"):
|
||||
msg_dict["name"] = msg["name"]
|
||||
if msg.get("tool_calls"):
|
||||
msg_dict["tool_calls"] = msg["tool_calls"]
|
||||
request_messages.append(msg_dict)
|
||||
|
||||
request_params = {
|
||||
"model": self.genai_config.model,
|
||||
"messages": request_messages,
|
||||
}
|
||||
|
||||
if tools:
|
||||
request_params["tools"] = tools
|
||||
if tool_choice:
|
||||
if tool_choice == "none":
|
||||
request_params["tool_choice"] = "none"
|
||||
elif tool_choice == "required":
|
||||
request_params["tool_choice"] = "required"
|
||||
elif tool_choice == "auto":
|
||||
request_params["tool_choice"] = "auto"
|
||||
|
||||
request_params.update(self.provider_options)
|
||||
|
||||
response = self.provider.chat(**request_params)
|
||||
|
||||
if not response or "message" not in response:
|
||||
return {
|
||||
"content": None,
|
||||
"tool_calls": None,
|
||||
"finish_reason": "error",
|
||||
}
|
||||
|
||||
message = response["message"]
|
||||
content = (
|
||||
message.get("content", "").strip() if message.get("content") else None
|
||||
request_params = self._build_request_params(
|
||||
messages, tools, tool_choice, stream=False
|
||||
)
|
||||
|
||||
tool_calls = None
|
||||
if "tool_calls" in message and message["tool_calls"]:
|
||||
tool_calls = []
|
||||
for tool_call in message["tool_calls"]:
|
||||
try:
|
||||
function_data = tool_call.get("function", {})
|
||||
arguments_str = function_data.get("arguments", "{}")
|
||||
arguments = json.loads(arguments_str)
|
||||
except (json.JSONDecodeError, KeyError, TypeError) as e:
|
||||
logger.warning(
|
||||
f"Failed to parse tool call arguments: {e}, "
|
||||
f"tool: {function_data.get('name', 'unknown')}"
|
||||
)
|
||||
arguments = {}
|
||||
|
||||
tool_calls.append(
|
||||
{
|
||||
"id": tool_call.get("id", ""),
|
||||
"name": function_data.get("name", ""),
|
||||
"arguments": arguments,
|
||||
}
|
||||
)
|
||||
|
||||
finish_reason = "error"
|
||||
if "done" in response and response["done"]:
|
||||
if tool_calls:
|
||||
finish_reason = "tool_calls"
|
||||
elif content:
|
||||
finish_reason = "stop"
|
||||
elif tool_calls:
|
||||
finish_reason = "tool_calls"
|
||||
elif content:
|
||||
finish_reason = "stop"
|
||||
|
||||
return {
|
||||
"content": content,
|
||||
"tool_calls": tool_calls,
|
||||
"finish_reason": finish_reason,
|
||||
}
|
||||
|
||||
response = self.provider.chat(**request_params)
|
||||
return self._message_from_response(response)
|
||||
except (TimeoutException, ResponseError, ConnectionError) as e:
|
||||
logger.warning("Ollama returned an error: %s", str(e))
|
||||
return {
|
||||
@ -204,3 +191,89 @@ class OllamaClient(GenAIClient):
|
||||
"tool_calls": None,
|
||||
"finish_reason": "error",
|
||||
}
|
||||
|
||||
async def chat_with_tools_stream(
|
||||
self,
|
||||
messages: list[dict[str, Any]],
|
||||
tools: Optional[list[dict[str, Any]]] = None,
|
||||
tool_choice: Optional[str] = "auto",
|
||||
):
|
||||
"""Stream chat with tools; yields content deltas then final message."""
|
||||
if self.provider is None:
|
||||
logger.warning(
|
||||
"Ollama provider has not been initialized. Check your Ollama configuration."
|
||||
)
|
||||
yield (
|
||||
"message",
|
||||
{
|
||||
"content": None,
|
||||
"tool_calls": None,
|
||||
"finish_reason": "error",
|
||||
},
|
||||
)
|
||||
return
|
||||
try:
|
||||
request_params = self._build_request_params(
|
||||
messages, tools, tool_choice, stream=True
|
||||
)
|
||||
async_client = OllamaAsyncClient(
|
||||
host=self.genai_config.base_url,
|
||||
timeout=self.timeout,
|
||||
)
|
||||
content_parts: list[str] = []
|
||||
final_message: dict[str, Any] | None = None
|
||||
try:
|
||||
stream = await async_client.chat(**request_params)
|
||||
async for chunk in stream:
|
||||
if not chunk or "message" not in chunk:
|
||||
continue
|
||||
msg = chunk.get("message", {})
|
||||
delta = msg.get("content") or ""
|
||||
if delta:
|
||||
content_parts.append(delta)
|
||||
yield ("content_delta", delta)
|
||||
if chunk.get("done"):
|
||||
full_content = "".join(content_parts).strip() or None
|
||||
tool_calls = parse_tool_calls_from_message(msg)
|
||||
final_message = {
|
||||
"content": full_content,
|
||||
"tool_calls": tool_calls,
|
||||
"finish_reason": "tool_calls" if tool_calls else "stop",
|
||||
}
|
||||
break
|
||||
finally:
|
||||
await async_client.close()
|
||||
|
||||
if final_message is not None:
|
||||
yield ("message", final_message)
|
||||
else:
|
||||
yield (
|
||||
"message",
|
||||
{
|
||||
"content": "".join(content_parts).strip() or None,
|
||||
"tool_calls": None,
|
||||
"finish_reason": "stop",
|
||||
},
|
||||
)
|
||||
except (TimeoutException, ResponseError, ConnectionError) as e:
|
||||
logger.warning("Ollama streaming error: %s", str(e))
|
||||
yield (
|
||||
"message",
|
||||
{
|
||||
"content": None,
|
||||
"tool_calls": None,
|
||||
"finish_reason": "error",
|
||||
},
|
||||
)
|
||||
except Exception as e:
|
||||
logger.warning(
|
||||
"Unexpected error in Ollama chat_with_tools_stream: %s", str(e)
|
||||
)
|
||||
yield (
|
||||
"message",
|
||||
{
|
||||
"content": None,
|
||||
"tool_calls": None,
|
||||
"finish_reason": "error",
|
||||
},
|
||||
)
|
||||
|
||||
70
frigate/genai/utils.py
Normal file
70
frigate/genai/utils.py
Normal file
@ -0,0 +1,70 @@
|
||||
"""Shared helpers for GenAI providers and chat (OpenAI-style messages, tool call parsing)."""
|
||||
|
||||
import json
|
||||
import logging
|
||||
from typing import Any, List, Optional
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def parse_tool_calls_from_message(
|
||||
message: dict[str, Any],
|
||||
) -> Optional[list[dict[str, Any]]]:
|
||||
"""
|
||||
Parse tool_calls from an OpenAI-style message dict.
|
||||
|
||||
Message may have "tool_calls" as a list of:
|
||||
{"id": str, "function": {"name": str, "arguments": str}, ...}
|
||||
|
||||
Returns a list of {"id", "name", "arguments"} with arguments parsed as dict,
|
||||
or None if no tool_calls. Used by Ollama and LlamaCpp (non-stream) responses.
|
||||
"""
|
||||
raw = message.get("tool_calls")
|
||||
if not raw or not isinstance(raw, list):
|
||||
return None
|
||||
result = []
|
||||
for tool_call in raw:
|
||||
function_data = tool_call.get("function") or {}
|
||||
try:
|
||||
arguments_str = function_data.get("arguments") or "{}"
|
||||
arguments = json.loads(arguments_str)
|
||||
except (json.JSONDecodeError, KeyError, TypeError) as e:
|
||||
logger.warning(
|
||||
"Failed to parse tool call arguments: %s, tool: %s",
|
||||
e,
|
||||
function_data.get("name", "unknown"),
|
||||
)
|
||||
arguments = {}
|
||||
result.append(
|
||||
{
|
||||
"id": tool_call.get("id", ""),
|
||||
"name": function_data.get("name", ""),
|
||||
"arguments": arguments,
|
||||
}
|
||||
)
|
||||
return result if result else None
|
||||
|
||||
|
||||
def build_assistant_message_for_conversation(
|
||||
content: Any,
|
||||
tool_calls_raw: Optional[List[dict[str, Any]]],
|
||||
) -> dict[str, Any]:
|
||||
"""
|
||||
Build the assistant message dict in OpenAI format for appending to a conversation.
|
||||
|
||||
tool_calls_raw: list of {"id", "name", "arguments"} (arguments as dict), or None.
|
||||
"""
|
||||
msg: dict[str, Any] = {"role": "assistant", "content": content}
|
||||
if tool_calls_raw:
|
||||
msg["tool_calls"] = [
|
||||
{
|
||||
"id": tc["id"],
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": tc["name"],
|
||||
"arguments": json.dumps(tc.get("arguments") or {}),
|
||||
},
|
||||
}
|
||||
for tc in tool_calls_raw
|
||||
]
|
||||
return msg
|
||||
1458
web/package-lock.json
generated
1458
web/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -71,6 +71,8 @@
|
||||
"react-icons": "^5.5.0",
|
||||
"react-konva": "^18.2.10",
|
||||
"react-router-dom": "^6.30.3",
|
||||
"react-markdown": "^9.0.1",
|
||||
"remark-gfm": "^4.0.0",
|
||||
"react-swipeable": "^7.0.2",
|
||||
"react-tracked": "^2.0.1",
|
||||
"react-transition-group": "^4.4.5",
|
||||
|
||||
@ -6,7 +6,8 @@
|
||||
"title": "Frigate s'està reiniciant",
|
||||
"content": "Aquesta pàgina es tornarà a carregar d'aquí a {{countdown}} segons.",
|
||||
"button": "Forçar la recàrrega ara"
|
||||
}
|
||||
},
|
||||
"description": "Això aturarà breument Frigate mentre es reinicia."
|
||||
},
|
||||
"explore": {
|
||||
"plus": {
|
||||
|
||||
@ -114,6 +114,11 @@
|
||||
},
|
||||
"error": {
|
||||
"mustBeFinished": "El dibuix del polígon s'ha d'acabar abans de desar."
|
||||
},
|
||||
"type": {
|
||||
"zone": "zona",
|
||||
"motion_mask": "màscara de moviment",
|
||||
"object_mask": "màscara d'objecte"
|
||||
}
|
||||
},
|
||||
"zoneName": {
|
||||
|
||||
@ -6,7 +6,8 @@
|
||||
"title": "Frigate genstarter",
|
||||
"button": "Gennemtving genindlæsning nu",
|
||||
"content": "Denne side genindlæses om {{countdown}} sekunder."
|
||||
}
|
||||
},
|
||||
"description": "Dette vil kortvarigt stoppe Frigate under genstart."
|
||||
},
|
||||
"explore": {
|
||||
"plus": {
|
||||
@ -17,7 +18,9 @@
|
||||
"review": {
|
||||
"question": {
|
||||
"label": "Bekræft denne etiket til Frigate Plus",
|
||||
"ask_a": "Er dette objekt et <code>{{label}}</code>?"
|
||||
"ask_a": "Er dette objekt et <code>{{label}}</code>?",
|
||||
"ask_an": "Er dette objekt en <code>{{label}}</code>?",
|
||||
"ask_full": "Er dette objekt en <code>{{untranslatedLabel}}</code> ({{translatedLabel}})?"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
{
|
||||
"filter": "Filter",
|
||||
"filter": "Filtrer",
|
||||
"classes": {
|
||||
"label": "Klasser",
|
||||
"all": {
|
||||
|
||||
@ -27,5 +27,6 @@
|
||||
"markTheseItemsAsReviewed": "Marker disse som gennemset",
|
||||
"detail": {
|
||||
"aria": "Skift til detaljevisning"
|
||||
}
|
||||
},
|
||||
"timeline.aria": "Vælg tidslinje"
|
||||
}
|
||||
|
||||
@ -17,10 +17,15 @@
|
||||
"context": "Udforsk kan bruges, når genindekseringen af de sporede objektindlejringer er fuldført.",
|
||||
"finishingShortly": "Afsluttes om lidt",
|
||||
"step": {
|
||||
"thumbnailsEmbedded": "Miniaturer indlejret: "
|
||||
"thumbnailsEmbedded": "Miniaturer indlejret: ",
|
||||
"descriptionsEmbedded": "Beskrivelser indlejrede: ",
|
||||
"trackedObjectsProcessed": "Sporede objekter behandlede: "
|
||||
}
|
||||
},
|
||||
"title": "Udforsk er ikke tilgængelig"
|
||||
"title": "Udforsk er ikke tilgængelig",
|
||||
"downloadingModels": {
|
||||
"context": "Frigate henter de nødvendige indlejringsmodeller for at understøtte semantiske søgninger. Dette kan tage flere minutter, afhængig af hastigheden på din netværksforbindelse."
|
||||
}
|
||||
},
|
||||
"exploreMore": "Udforsk flere {{label}}-objekter",
|
||||
"details": {
|
||||
|
||||
@ -3,7 +3,8 @@
|
||||
"description": {
|
||||
"addFace": "Tilføj en ny samling til ansigtsbiblioteket ved at uploade dit første billede.",
|
||||
"placeholder": "Angiv et navn for bibliotek",
|
||||
"invalidName": "Ugyldigt navn. Navne må kun indeholde bogstaver, tal, mellemrum, apostroffer, understregninger og bindestreger."
|
||||
"invalidName": "Ugyldigt navn. Navne må kun indeholde bogstaver, tal, mellemrum, apostroffer, understregninger og bindestreger.",
|
||||
"nameCannotContainHash": "Navet kan ikke indeholde #."
|
||||
},
|
||||
"details": {
|
||||
"person": "Person",
|
||||
@ -17,6 +18,76 @@
|
||||
"desc": "Upload et billede for at scanne efter ansigter og inkludere det for {{pageToggle}}"
|
||||
},
|
||||
"train": {
|
||||
"titleShort": "Nyeste"
|
||||
}
|
||||
"titleShort": "Nyeste",
|
||||
"title": "Seneste genkendelser",
|
||||
"aria": "Vælg seneste genkendelser",
|
||||
"empty": "Der er ingen nylige ansigtsgenkendelser"
|
||||
},
|
||||
"createFaceLibrary": {
|
||||
"new": "Nyt ansigt",
|
||||
"nextSteps": "<ul> <li>Brug fanen <strong>Seneste genkendelser</strong> til at udvælge og træne på billeder for hver registreret person.</li> <li>Fokusér på billeder taget lige forfra for de bedste resultater; undgå træningsbilleder, hvor ansigter er fotograferet fra siden eller i vinkel.</li> </ul>"
|
||||
},
|
||||
"steps": {
|
||||
"faceName": "Skriv ansigt navn",
|
||||
"uploadFace": "Upload ansigt billede",
|
||||
"nextSteps": "Næste skridt",
|
||||
"description": {
|
||||
"uploadFace": "Upload et billede af {{name}}, hvor ansigtet er set forfra. Billedet behøver ikke kun at vise ansigtet og skal ikke beskæres."
|
||||
}
|
||||
},
|
||||
"button": {
|
||||
"deleteFace": "Slet ansigt",
|
||||
"deleteFaceAttempts": "Slet ansigter",
|
||||
"addFace": "Tilføj ansigt",
|
||||
"renameFace": "Omdøb ansigt",
|
||||
"uploadImage": "Upload billede",
|
||||
"reprocessFace": "Genbehandl ansigt"
|
||||
},
|
||||
"trainFace": "Lær ansigt",
|
||||
"renameFace": {
|
||||
"title": "Omdøb ansigt",
|
||||
"desc": "Indtast et nyt navn til {{name}}"
|
||||
},
|
||||
"toast": {
|
||||
"success": {
|
||||
"deletedFace_one": "{{count}} ansigt blev slettet",
|
||||
"deletedFace_other": "{{count}} ansigter blev slettet",
|
||||
"deletedName_one": "{{count}} ansigt slettet",
|
||||
"deletedName_other": "{{count}} ansigter slettet",
|
||||
"uploadedImage": "Billedet blev uploadet.",
|
||||
"addFaceLibrary": "{{name}} er blevet tilføjet til ansigtsbiblioteket!",
|
||||
"renamedFace": "Ansigtet er blevet omdøbt til {{name}}",
|
||||
"trainedFace": "Ansigtet er blevet trænet.",
|
||||
"updatedFaceScore": "Ansigtets score er blevet opdateret til {{score}} ({{name}})."
|
||||
},
|
||||
"error": {
|
||||
"uploadingImageFailed": "Kunne ikke uploade billedet: {{errorMessage}}",
|
||||
"addFaceLibraryFailed": "Kunne ikke angive navn på ansigtet: {{errorMessage}}",
|
||||
"deleteFaceFailed": "Kunne ikke slette: {{errorMessage}}",
|
||||
"deleteNameFailed": "Kunne ikke slette navnet: {{errorMessage}}",
|
||||
"renameFaceFailed": "Kunne ikke omdøbe ansigtet: {{errorMessage}}",
|
||||
"trainFailed": "Kunne ikke træne: {{errorMessage}}",
|
||||
"updateFaceScoreFailed": "Kunne ikke opdatere ansigtets score: {{errorMessage}}"
|
||||
}
|
||||
},
|
||||
"deleteFaceAttempts": {
|
||||
"desc_one": "Er du sikker på, at du vil slette {{count}} ansigt? Denne handling kan ikke fortrydes.",
|
||||
"desc_other": "Er du sikker på, at du vil slette {{count}} ansigter? Denne handling kan ikke fortrydes.",
|
||||
"title": "Slet ansigter"
|
||||
},
|
||||
"collections": "Samlinger",
|
||||
"deleteFaceLibrary": {
|
||||
"title": "Slet navn",
|
||||
"desc": "Er du sikker på, at du vil slette samlingen {{name}}? Dette vil permanent slette alle tilknyttede ansigter."
|
||||
},
|
||||
"imageEntry": {
|
||||
"maxSize": "Maks. størrelse: {{size}} MB",
|
||||
"validation": {
|
||||
"selectImage": "Vælg venligst en billedfil."
|
||||
},
|
||||
"dropActive": "Slip billedet her…",
|
||||
"dropInstructions": "Træk og slip eller indsæt et billede her – eller klik for at vælge"
|
||||
},
|
||||
"nofaces": "Ingen tilgængelige ansigter",
|
||||
"trainFaceAs": "Træn ansigt som:"
|
||||
}
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
{
|
||||
"filter": "Filter",
|
||||
"filter": "Filtrer",
|
||||
"export": "Eksporter",
|
||||
"calendar": "Kalender",
|
||||
"filters": "Filtere",
|
||||
|
||||
@ -9,5 +9,11 @@
|
||||
"filterActive": "Filtre aktiv",
|
||||
"clear": "Ryd søgning"
|
||||
},
|
||||
"trackedObjectId": "Sporet genstands-ID"
|
||||
"trackedObjectId": "Sporet genstands-ID",
|
||||
"filter": {
|
||||
"label": {
|
||||
"cameras": "Kameraer",
|
||||
"zones": "Områder"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -9,6 +9,11 @@
|
||||
"enrichments": "Indstillinger for berigelser - Frigate",
|
||||
"masksAndZones": "Maske- og zoneeditor - Frigate",
|
||||
"motionTuner": "Bevægelsesjustering - Frigate",
|
||||
"general": "Brugergrænsefladeindstillinger - Frigate"
|
||||
"general": "Brugergrænsefladeindstillinger - Frigate",
|
||||
"frigatePlus": "Frigate+ Indstillinger - Frigate",
|
||||
"notifications": "Notifikations indstillinger - Frigate"
|
||||
},
|
||||
"menu": {
|
||||
"ui": "Brugergrænseflade"
|
||||
}
|
||||
}
|
||||
|
||||
@ -6,7 +6,8 @@
|
||||
"content": "Diese Seite wird in {{countdown}} Sekunde(n) aktualisiert.",
|
||||
"button": "Neuladen erzwingen"
|
||||
},
|
||||
"button": "Neustarten"
|
||||
"button": "Neustarten",
|
||||
"description": "Dies wird Frigate kurz stoppen, während es neu startet."
|
||||
},
|
||||
"explore": {
|
||||
"plus": {
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
{
|
||||
"description": {
|
||||
"placeholder": "Gib einen Name für diese Kollektion ein",
|
||||
"addFace": "Füge der Gesichtsbibliothek eine neue Sammlung hinzu, indem du ein Bild hochlädst.",
|
||||
"addFace": "Füge der Gesichtsbibliothek eine neue Sammlung hinzu, indem du ein erstes Bild hochlädst.",
|
||||
"invalidName": "Ungültiger Name. Namen dürfen nur Buchstaben, Zahlen, Leerzeichen, Apostrophe, Unterstriche und Bindestriche enthalten.",
|
||||
"nameCannotContainHash": "Der Name darf keine # enthalten."
|
||||
},
|
||||
|
||||
@ -49,12 +49,12 @@
|
||||
"desc": "Standardmäßig werden die letzten Warnmeldungen auf dem Live-Dashboard als kurze Videoschleifen abgespielt. Deaktiviere diese Option, um nur ein statisches Bild der letzten Warnungen auf diesem Gerät/Browser anzuzeigen."
|
||||
},
|
||||
"automaticLiveView": {
|
||||
"desc": "Zeigt automatisch das Live-Bild einer Kamera an, wenn eine Aktivität erkannt wird. Ist diese Option deaktiviert, werden Kamerabilder im Live-Dashboard nur einmal pro Minute aktualisiert.",
|
||||
"desc": "Automatisch zur Live-Ansicht einer Kamera wechseln, wenn eine Aktivität erkannt wird. Wenn diese Option deaktiviert ist, werden statische Kamerabilder auf dem Live-Dashboard nur einmal pro Minute aktualisiert.",
|
||||
"label": "Automatische Live Ansicht"
|
||||
},
|
||||
"displayCameraNames": {
|
||||
"label": "Immer Namen der Kamera anzeigen",
|
||||
"desc": "Kamerabezeichnung immer im einem Chip im Live-View-Dashboard für mehrere Kameras anzeigen."
|
||||
"desc": "Zeige immer die Kameranamen in einem Chip im Dashboard der Mehrkamera-Live-Ansicht an."
|
||||
},
|
||||
"liveFallbackTimeout": {
|
||||
"label": "Live Player Ausfallzeitlimit",
|
||||
@ -276,6 +276,11 @@
|
||||
},
|
||||
"error": {
|
||||
"mustBeFinished": "Polygonzeichnung muss vor dem Speichern abgeschlossen sein."
|
||||
},
|
||||
"type": {
|
||||
"zone": "Zone",
|
||||
"motion_mask": "Bewegungsmaske",
|
||||
"object_mask": "Objektmaske"
|
||||
}
|
||||
},
|
||||
"speed": {
|
||||
@ -1210,7 +1215,7 @@
|
||||
"restreamingWarning": "Die Reduzierung der Verbindungen zur Kamera für den Aufzeichnungsstream kann zu einer geringfügigen Erhöhung der CPU-Auslastung führen.",
|
||||
"brands": {
|
||||
"reolink-rtsp": "Reolink RTSP wird nicht empfohlen. Aktivieren Sie HTTP in den Firmware-Einstellungen der Kamera und starten Sie den Assistenten neu.",
|
||||
"reolink-http": "Für Reolink-HTTP-Streams sollten sie FFmpeg verwenden, um eine bessere Kompatibilität zu gewährleisten. Aktivieren Sie für diesen Stream die Option „Stream-Kompatibilitätsmodus verwenden“."
|
||||
"reolink-http": "Reolink-HTTP-Streams sollten für eine bessere Kompatibilität FFmpeg verwenden. Aktivieren Sie für diesen Stream die Option „Stream-Kompatibilitätsmodus verwenden“."
|
||||
},
|
||||
"dahua": {
|
||||
"substreamWarning": "Substream 1 ist auf eine niedrige Auflösung festgelegt. Viele Kameras von Dahua / Amcrest / EmpireTech unterstützen zusätzliche Substreams, die in den Kameraeinstellungen aktiviert werden müssen. Es wird empfohlen, diese Streams zu überprüfen und zu nutzen, sofern sie verfügbar sind."
|
||||
|
||||
@ -127,6 +127,7 @@
|
||||
"cancel": "Cancel",
|
||||
"close": "Close",
|
||||
"copy": "Copy",
|
||||
"copiedToClipboard": "Copied to clipboard",
|
||||
"back": "Back",
|
||||
"history": "History",
|
||||
"fullscreen": "Fullscreen",
|
||||
@ -245,6 +246,7 @@
|
||||
"uiPlayground": "UI Playground",
|
||||
"faceLibrary": "Face Library",
|
||||
"classification": "Classification",
|
||||
"chat": "Chat",
|
||||
"user": {
|
||||
"title": "User",
|
||||
"account": "Account",
|
||||
|
||||
24
web/public/locales/en/views/chat.json
Normal file
24
web/public/locales/en/views/chat.json
Normal file
@ -0,0 +1,24 @@
|
||||
{
|
||||
"title": "Frigate Chat",
|
||||
"subtitle": "Your AI assistant for camera management and insights",
|
||||
"placeholder": "Ask anything...",
|
||||
"error": "Something went wrong. Please try again.",
|
||||
"processing": "Processing...",
|
||||
"toolsUsed": "Used: {{tools}}",
|
||||
"showTools": "Show tools ({{count}})",
|
||||
"hideTools": "Hide tools",
|
||||
"call": "Call",
|
||||
"result": "Result",
|
||||
"arguments": "Arguments:",
|
||||
"response": "Response:",
|
||||
"send": "Send",
|
||||
"suggested_requests": "Try asking:",
|
||||
"starting_requests": {
|
||||
"show_recent_events": "Show recent events",
|
||||
"show_camera_status": "Show camera status"
|
||||
},
|
||||
"starting_requests_prompts": {
|
||||
"show_recent_events": "Show me the recent events from the last hour",
|
||||
"show_camera_status": "What is the current status of my cameras?"
|
||||
}
|
||||
}
|
||||
@ -153,7 +153,8 @@
|
||||
"bg": "Български (Búlgaro)",
|
||||
"gl": "Galego (Gallego)",
|
||||
"id": "Bahasa Indonesia (Indonesio)",
|
||||
"ur": "اردو (Urdu)"
|
||||
"ur": "اردو (Urdu)",
|
||||
"hr": "Hrvatski (Croata)"
|
||||
},
|
||||
"appearance": "Apariencia",
|
||||
"darkMode": {
|
||||
|
||||
@ -6,7 +6,8 @@
|
||||
"content": "Esta página se recargará en {{countdown}} segundos."
|
||||
},
|
||||
"title": "¿Estás seguro de que quieres reiniciar Frigate?",
|
||||
"button": "Reiniciar"
|
||||
"button": "Reiniciar",
|
||||
"description": "Esto detendrá brevemente Frigate mientras se reinicia."
|
||||
},
|
||||
"explore": {
|
||||
"plus": {
|
||||
|
||||
@ -129,13 +129,13 @@
|
||||
"classes": {
|
||||
"label": "Clases",
|
||||
"all": {
|
||||
"title": "Todas las Clases"
|
||||
"title": "Todas las clases"
|
||||
},
|
||||
"count_one": "{{count}} Clase",
|
||||
"count_other": "{{count}} Clases"
|
||||
},
|
||||
"attributes": {
|
||||
"label": "Atributos de Clasificación",
|
||||
"label": "Atributos de clasificación",
|
||||
"all": "Todos los Atributos"
|
||||
}
|
||||
}
|
||||
|
||||
@ -2,7 +2,8 @@
|
||||
"description": {
|
||||
"addFace": "Agregar una nueva colección a la Biblioteca de Rostros subiendo tu primera imagen.",
|
||||
"placeholder": "Introduce un nombre para esta colección",
|
||||
"invalidName": "Nombre incorrecto. Los nombres solo pueden incluir letras, números, espacios, apóstrofes, guiones bajos, y guiones."
|
||||
"invalidName": "Nombre incorrecto. Los nombres solo pueden incluir letras, números, espacios, apóstrofes, guiones bajos, y guiones.",
|
||||
"nameCannotContainHash": "El nombre no puede contener #."
|
||||
},
|
||||
"details": {
|
||||
"person": "Persona",
|
||||
|
||||
@ -276,7 +276,12 @@
|
||||
"reset": {
|
||||
"label": "Borrar todos los puntos"
|
||||
},
|
||||
"removeLastPoint": "Eliminar el último punto"
|
||||
"removeLastPoint": "Eliminar el último punto",
|
||||
"type": {
|
||||
"zone": "zona",
|
||||
"motion_mask": "máscara de movimiento",
|
||||
"object_mask": "máscara de objeto"
|
||||
}
|
||||
},
|
||||
"speed": {
|
||||
"error": {
|
||||
@ -548,7 +553,7 @@
|
||||
"hide": "Ocultar contraseña",
|
||||
"requirements": {
|
||||
"title": "Requisitos de contraseña:",
|
||||
"length": "Al menos 8 caracteres",
|
||||
"length": "Al menos 12 caracteres",
|
||||
"uppercase": "Al menos una mayúscula",
|
||||
"digit": "Al menos un número",
|
||||
"special": "Al menos un caracter especial (!@#$%^&*(),.?\":{}|<>)"
|
||||
|
||||
@ -192,7 +192,7 @@
|
||||
"classification_speed": "Velocidad de clasificación de {{name}}",
|
||||
"classification_events_per_second": "Clasificacion de eventos por segundo de {{name}}"
|
||||
},
|
||||
"title": "Enriquicimientos",
|
||||
"title": "Enriquecimientos",
|
||||
"averageInf": "Tiempo promedio de inferencia"
|
||||
},
|
||||
"stats": {
|
||||
|
||||
@ -6,7 +6,8 @@
|
||||
"title": "Frigate käivitub uuesti",
|
||||
"content": "See leht laaditakse uuesti {{countdown}} sekundi pärast.",
|
||||
"button": "Laadi uuesti kohe"
|
||||
}
|
||||
},
|
||||
"description": "Järgnevaga Frigate uuesti käivitamise ajaks lõpetab korraks töö."
|
||||
},
|
||||
"search": {
|
||||
"saveSearch": {
|
||||
|
||||
@ -6,7 +6,8 @@
|
||||
"content": "Cette page sera rechargée dans {{countdown}} secondes.",
|
||||
"button": "Forcer l'actualisation maintenant"
|
||||
},
|
||||
"button": "Redémarrer"
|
||||
"button": "Redémarrer",
|
||||
"description": "Frigate s'arrêtera momentanément pour redémarrer."
|
||||
},
|
||||
"explore": {
|
||||
"plus": {
|
||||
|
||||
@ -366,6 +366,11 @@
|
||||
"snapPoints": {
|
||||
"true": "Points d'accrochage",
|
||||
"false": "Ne pas réunir les points"
|
||||
},
|
||||
"type": {
|
||||
"zone": "zone",
|
||||
"motion_mask": "masque de mouvement",
|
||||
"object_mask": "masque d'objet"
|
||||
}
|
||||
},
|
||||
"loiteringTime": {
|
||||
|
||||
@ -75,7 +75,8 @@
|
||||
"formattedTimestampMonthDay": "MMM d",
|
||||
"inProgress": "Folyamatban",
|
||||
"invalidStartTime": "Érvénytelen kezdeti idő",
|
||||
"never": "Soha"
|
||||
"never": "Soha",
|
||||
"invalidEndTime": "Érvénytelen befejezési idő"
|
||||
},
|
||||
"menu": {
|
||||
"darkMode": {
|
||||
@ -106,7 +107,7 @@
|
||||
"logout": "Kijelentkezés",
|
||||
"title": "Felhasználó",
|
||||
"account": "Fiók",
|
||||
"current": "Jelenlegi Felhazsnáló: {{user}}",
|
||||
"current": "Jelenlegi Felhasználó: {{user}}",
|
||||
"anonymous": "anoním",
|
||||
"setPassword": "Jelszó Beállítása"
|
||||
},
|
||||
@ -153,7 +154,8 @@
|
||||
"bg": "Български (Bolgár)",
|
||||
"gl": "Galego (Galíciai)",
|
||||
"id": "Bahasa Indonesia (Indonéz)",
|
||||
"ur": "اردو (Urdu)"
|
||||
"ur": "اردو (Urdu)",
|
||||
"hr": "Horvát"
|
||||
},
|
||||
"uiPlayground": "UI játszótér",
|
||||
"faceLibrary": "Arc Könyvtár",
|
||||
@ -175,7 +177,8 @@
|
||||
"system": "Rendszer",
|
||||
"configuration": "Konfiguráció",
|
||||
"systemLogs": "Rendszer naplók",
|
||||
"settings": "Beállítások"
|
||||
"settings": "Beállítások",
|
||||
"classification": "Osztályozás"
|
||||
},
|
||||
"role": {
|
||||
"viewer": "Néző",
|
||||
@ -215,7 +218,7 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"selectItem": "KIválasztani {{item}}-et",
|
||||
"selectItem": "Kiválasztani {{item}}-et",
|
||||
"unit": {
|
||||
"speed": {
|
||||
"mph": "mph",
|
||||
@ -269,14 +272,29 @@
|
||||
"unselect": "Kijelölés megszüntetése",
|
||||
"export": "Exportálás",
|
||||
"deleteNow": "Törlés Most",
|
||||
"next": "Következő"
|
||||
"next": "Következő",
|
||||
"continue": "Tovább"
|
||||
},
|
||||
"label": {
|
||||
"back": "Vissza",
|
||||
"all": "Mind"
|
||||
"all": "Mind",
|
||||
"hide": "Elrejt {{item}}",
|
||||
"show": "Mutat {{item}}",
|
||||
"ID": "ID",
|
||||
"none": "Nincs",
|
||||
"other": "Egyéb"
|
||||
},
|
||||
"readTheDocumentation": "Olvassa el a dokumentációt",
|
||||
"information": {
|
||||
"pixels": "{{area}}px"
|
||||
},
|
||||
"list": {
|
||||
"two": "{{0}} és {{1}}",
|
||||
"many": "{{items}}, és {{last}}",
|
||||
"separatorWithSpace": ", "
|
||||
},
|
||||
"field": {
|
||||
"optional": "Opcionális",
|
||||
"internalID": "A belső ID, amelyet a Frigate használ a konfigurációban és az adatbázisban"
|
||||
}
|
||||
}
|
||||
|
||||
@ -6,7 +6,8 @@
|
||||
"title": "A Frigate újraindul",
|
||||
"content": "Az oldal újratölt {{countdown}} másodperc múlva.",
|
||||
"button": "Erőltetett újraindítás azonnal"
|
||||
}
|
||||
},
|
||||
"description": "Ez rövid időre leállítja a Frigate programot, amíg újraindul."
|
||||
},
|
||||
"explore": {
|
||||
"plus": {
|
||||
@ -57,7 +58,8 @@
|
||||
"failed": "Nem sikerült elkezdeni az exportálást: {{error}}",
|
||||
"endTimeMustAfterStartTime": "A végső időpontnak a kezdeti időpont után kell következnie",
|
||||
"noVaildTimeSelected": "Nincs érvényes idő intervallum kiválasztva"
|
||||
}
|
||||
},
|
||||
"view": "Megtekint"
|
||||
},
|
||||
"fromTimeline": {
|
||||
"saveExport": "Exportálás mentése",
|
||||
|
||||
@ -13,8 +13,8 @@
|
||||
"toast": {
|
||||
"success": {
|
||||
"deletedImage": "Törölt képek",
|
||||
"deletedModel_one": "Sikeresen törölt {{count}} modellt",
|
||||
"deletedModel_other": "",
|
||||
"deletedModel_one": "Sikeresen törölve {{count}} modell",
|
||||
"deletedModel_other": "Sikeresen törölve {{count}} modell",
|
||||
"categorizedImage": "A kép sikeresen osztályozva",
|
||||
"deletedCategory": "Osztály törlése",
|
||||
"trainedModel": "Sikeresen betanított modell.",
|
||||
@ -24,7 +24,13 @@
|
||||
},
|
||||
"error": {
|
||||
"deleteImageFailed": "Törlés sikertelen: {{errorMessage}}",
|
||||
"deleteCategoryFailed": "Nem sikerült törölni az osztályt: {{errorMessage}}"
|
||||
"deleteCategoryFailed": "Nem sikerült törölni az osztályt: {{errorMessage}}",
|
||||
"deleteModelFailed": "Modell törlése nem sikerült: {{errorMessage}}",
|
||||
"categorizeFailed": "A kép kategorizálása sikertelen: {{errorMessage}}",
|
||||
"trainingFailed": "A modell képzése sikertelen volt. A részletek a Frigate naplóiban találhatók.",
|
||||
"trainingFailedToStart": "A modell képzésének elindítása sikertelen: {{errorMessage}}",
|
||||
"updateModelFailed": "A modell frissítése sikertelen: {{errorMessage}}",
|
||||
"renameCategoryFailed": "Az osztály átnevezése sikertelen: {{errorMessage}}"
|
||||
}
|
||||
},
|
||||
"details": {
|
||||
@ -54,5 +60,16 @@
|
||||
},
|
||||
"train": {
|
||||
"titleShort": "Friss"
|
||||
},
|
||||
"deleteCategory": {
|
||||
"title": "Osztály törlése",
|
||||
"desc": "Biztosan törölni szeretné a {{name}} osztályt? Ezzel véglegesen törli az összes kapcsolódó képet, és a modell újratanítására lesz szükség.",
|
||||
"minClassesTitle": "Osztály törlése nem lehetséges"
|
||||
},
|
||||
"deleteModel": {
|
||||
"title": "Osztályozási modell törlése",
|
||||
"single": "Biztosan törölni szeretné a(z) {{name}}-t? Ezzel véglegesen törli az összes kapcsolódó adatot, beleértve a képeket és a tanítási adatokat is. Ez a művelet visszafordíthatatlan.",
|
||||
"desc_one": "Biztosan törölni szeretné a(z) {{count}} modellt? Ezzel véglegesen törli az összes kapcsolódó adatot, beleértve a képeket és a tanítási adatokat is. Ez a művelet visszafordíthatatlan.",
|
||||
"desc_other": "Biztosan törölni szeretné a(z) {{count}} modelleket? Ezzel véglegesen törli az összes kapcsolódó adatot, beleértve a képeket és a tanítási adatokat is. Ez a művelet visszafordíthatatlan."
|
||||
}
|
||||
}
|
||||
|
||||
@ -54,5 +54,12 @@
|
||||
"alwaysExpandActive": {
|
||||
"title": "Mindig kibontja az aktív részt"
|
||||
}
|
||||
}
|
||||
},
|
||||
"objectTrack": {
|
||||
"trackedPoint": "Nyomon követett pont",
|
||||
"clickToSeek": "Kattintson, az időponthoz ugráshoz"
|
||||
},
|
||||
"select_all": "Összes",
|
||||
"needsReview": "Felülvizsgálatra szorul",
|
||||
"securityConcern": "Biztonsági aggályok"
|
||||
}
|
||||
|
||||
@ -235,10 +235,19 @@
|
||||
"header": {
|
||||
"zones": "Zónák",
|
||||
"ratio": "Arány",
|
||||
"area": "Terület"
|
||||
}
|
||||
"area": "Terület",
|
||||
"score": "Pontszám"
|
||||
},
|
||||
"visible": "{{label}} észlelve",
|
||||
"entered_zone": "{{label}} belépett {{zones}}",
|
||||
"gone": "{{label}} maradt"
|
||||
},
|
||||
"title": "Követési adatok",
|
||||
"noImageFound": "Nem található kép ehhez az időbélyeghez."
|
||||
"noImageFound": "Nem található kép ehhez az időbélyeghez.",
|
||||
"createObjectMask": "Objektum maszk létrehozása",
|
||||
"scrollViewTips": "Kattintson ide, hogy megtekintse az objektum életciklusának fontosabb pillanatait.",
|
||||
"autoTrackingTips": "Az automatikus követésű kamerák esetében a keret pozíciói pontatlanok lesznek.",
|
||||
"count": "{{first}} a {{second}} közül",
|
||||
"trackedPoint": "Nyomon követett pont"
|
||||
}
|
||||
}
|
||||
|
||||
@ -47,7 +47,8 @@
|
||||
"description": {
|
||||
"placeholder": "Adj nevet ennek a gyűjteménynek",
|
||||
"invalidName": "Nem megfelelő név. A nevek csak betűket, számokat, szóközöket, aposztrófokat, alulhúzásokat és kötőjeleket tartalmazhatnak.",
|
||||
"addFace": "Adj hozzá egy új gyűjteményt az Arcképtárhoz az első képed feltöltésével."
|
||||
"addFace": "Adj hozzá egy új gyűjteményt az Arcképtárhoz az első képed feltöltésével.",
|
||||
"nameCannotContainHash": "A név nem tartalmazhat # karaktert."
|
||||
},
|
||||
"selectFace": "Arc kiválasztása",
|
||||
"deleteFaceLibrary": {
|
||||
@ -71,7 +72,7 @@
|
||||
"deletedName_one": "{{count}} arc sikeresen törölve.",
|
||||
"deletedName_other": "{{count}} arc sikeresen törölve.",
|
||||
"renamedFace": "Arc sikeresen átnvezezve {{name}}-ra/-re",
|
||||
"updatedFaceScore": "Arc pontszáma sikeresen frissítve.",
|
||||
"updatedFaceScore": "Arc pontszáma sikeresen frissítve a következőhöz {{name}} ({{score}}).",
|
||||
"trainedFace": "Arc sikeresen betanítva.",
|
||||
"deletedFace_one": "{{count}} arc sikeresen törölve.",
|
||||
"deletedFace_other": "{{count}} arc sikeresen törölve."
|
||||
|
||||
@ -50,6 +50,12 @@
|
||||
"playAlertVideos": {
|
||||
"label": "Riasztási Videók Lejátszása",
|
||||
"desc": "Alapértelmezetten az Élő irányítópulton a legutóbbi riasztások kis, ismétlődő videóként jelennek meg. Kapcsolja ki ezt az opciót, ha csak állóképet szeretne megjeleníteni a legutóbbi riasztásokról ezen az eszközön/böngészőben."
|
||||
},
|
||||
"displayCameraNames": {
|
||||
"label": "Mindig mutatja a kamera nevét"
|
||||
},
|
||||
"liveFallbackTimeout": {
|
||||
"desc": "Ha a kamera kiváló minőségű élő közvetítése nem elérhető, ennyi másodperc elteltével váltson alacsony sávszélességű módra. Alapértelmezett: 3."
|
||||
}
|
||||
},
|
||||
"title": "Alapbeállítások",
|
||||
@ -806,7 +812,7 @@
|
||||
"updateCameras": "Kamerák frissítve a szerepkörhöz: {{role}}",
|
||||
"deleteRole": "Szerepkör sikeresen törölve: {{role}}",
|
||||
"userRolesUpdated_one": "{{count}} felhasználó, akit ehhez a szerepkörhöz rendeltünk, frissült „néző”-re, amely hozzáféréssel rendelkezik az összes kamerához.",
|
||||
"userRolesUpdated_other": ""
|
||||
"userRolesUpdated_other": "{{count}} felhasználó, akit ehhez a szerepkörhöz rendeltünk, frissült „néző”-re, amely hozzáféréssel rendelkezik az összes kamerához."
|
||||
},
|
||||
"error": {
|
||||
"createRoleFailed": "Nem sikerült létrehozni a szerepkört: {{errorMessage}}",
|
||||
|
||||
@ -66,7 +66,7 @@
|
||||
"type": {
|
||||
"label": "Típus",
|
||||
"timestamp": "Időbélyeg",
|
||||
"tag": "Cédula",
|
||||
"tag": "Címke",
|
||||
"message": "Üzenet"
|
||||
},
|
||||
"toast": {
|
||||
|
||||
@ -6,7 +6,8 @@
|
||||
"title": "Sedang Merestart Frigate",
|
||||
"content": "Halaman ini akan memulai ulang dalam {{countdown}} detik.",
|
||||
"button": "Muat Ulang Sekarang"
|
||||
}
|
||||
},
|
||||
"description": "Layanan Frigate akan terhenti sejenak saat proses restart."
|
||||
},
|
||||
"explore": {
|
||||
"plus": {
|
||||
|
||||
@ -1,12 +1,14 @@
|
||||
{
|
||||
"documentTitle": "Klasifikasi Model - Frigate",
|
||||
"details": {
|
||||
"scoreInfo": "Skor tersebut mewakili rata-rata kepercayaan klasifikasi di seluruh deteksi objek ini."
|
||||
"scoreInfo": "Skor tersebut mewakili rata-rata kepercayaan klasifikasi di seluruh deteksi objek ini.",
|
||||
"none": "Tidak ada",
|
||||
"unknown": "Tidak diketahui"
|
||||
},
|
||||
"button": {
|
||||
"deleteClassificationAttempts": "Hapus Gambar Klasifikasi",
|
||||
"renameCategory": "Ubah Nama Kelas",
|
||||
"deleteCategory": "Hapus Kelas",
|
||||
"renameCategory": "Ganti Nama Class",
|
||||
"deleteCategory": "Hapus Class",
|
||||
"deleteImages": "Hapus Gambar",
|
||||
"trainModel": "Latih Model",
|
||||
"addClassification": "Tambah Klasifikasi",
|
||||
@ -14,14 +16,14 @@
|
||||
"editModel": "Ubah Model"
|
||||
},
|
||||
"tooltip": {
|
||||
"trainingInProgress": "Model dalam training",
|
||||
"noNewImages": "Tidak ada gambar untuk dilatih. Klasifikasikan gambar terlebih dahulu di dataset.",
|
||||
"trainingInProgress": "Model sedang training",
|
||||
"noNewImages": "Tidak ada gambar baru untuk training. Klasifikasi lebih banyak gambar di dataset terlebih dahulu.",
|
||||
"noChanges": "Tidak ada perubahan dataset sejak latihan terakhir.",
|
||||
"modelNotReady": "Model tidak siap untuk dilatih"
|
||||
},
|
||||
"toast": {
|
||||
"success": {
|
||||
"deletedCategory": "Kelas dihapus",
|
||||
"deletedCategory": "Class Dihapus",
|
||||
"deletedImage": "Image dihapus",
|
||||
"deletedModel_other": "Berhasil menghapus {{count}} model",
|
||||
"categorizedImage": "Berhasil Mengklasifikasikan Gambar",
|
||||
@ -31,17 +33,61 @@
|
||||
"renamedCategory": "Berhasil mengganti nama class ke {{name}}"
|
||||
},
|
||||
"error": {
|
||||
"updateModelFailed": "Gagal melakukan perubahan pada model: {{errorMessage}}",
|
||||
"updateModelFailed": "Gagal update model: {{errorMessage}}",
|
||||
"renameCategoryFailed": "Gagal merubah penamaan kelas: {{errorMessage}}",
|
||||
"deleteImageFailed": "Gagal menghapus: {{errorMessage}}",
|
||||
"deleteCategoryFailed": "Gagal menghapus kelas: {{errorMessage}}"
|
||||
"deleteCategoryFailed": "Gagal menghapus kelas: {{errorMessage}}",
|
||||
"deleteModelFailed": "Gagal menghapus model: {{errorMessage}}",
|
||||
"categorizeFailed": "Gagal mengkategorikan gambar: {{errorMessage}}",
|
||||
"trainingFailed": "Gagal melakukan training model. Cek log Frigate untuk rinciannya.",
|
||||
"trainingFailedToStart": "Gagal memulai training model: {{errorMessage}}"
|
||||
}
|
||||
},
|
||||
"deleteCategory": {
|
||||
"title": "Kelas dihapus",
|
||||
"minClassesTitle": "Dilarang menghapus Kelas"
|
||||
"minClassesTitle": "Dilarang menghapus Kelas",
|
||||
"desc": "Apakah Anda yakin ingin menghapus class {{name}}? Ini akan menghapus semua gambar terkait secara permanen dan memerlukan re-training model.",
|
||||
"minClassesDesc": "Model klasifikasi harus memiliki setidaknya 2 class. Tambahkan class lain sebelum menghapus yang ini."
|
||||
},
|
||||
"train": {
|
||||
"titleShort": "Terkini"
|
||||
},
|
||||
"wizard": {
|
||||
"title": "Buat Klasifikasi Baru",
|
||||
"steps": {
|
||||
"nameAndDefine": "Nama & Definisi",
|
||||
"stateArea": "Pilih Area",
|
||||
"chooseExamples": "Pilih Contoh"
|
||||
},
|
||||
"step1": {
|
||||
"description": "State model memantau area kamera yang tetap untuk setiap perubahan (contoh: pintu terbuka/tertutup). Object model menambahkan klasifikasi pada objek yang terdeteksi (contoh: hewan tertentu, kurir, dll.).",
|
||||
"name": "Nama",
|
||||
"namePlaceholder": "Masukkan nama model...",
|
||||
"type": "Tipe",
|
||||
"typeState": "Status",
|
||||
"typeObject": "Objek",
|
||||
"objectLabel": "Label Objek",
|
||||
"objectLabelPlaceholder": "Pilih tipe objek...",
|
||||
"classificationType": "Pilih Klasifikasi",
|
||||
"classificationTypeTip": "Pelajari tentang tipe klasifikasi",
|
||||
"classificationTypeDesc": "Sub Label menambahkan teks tambahan pada label objek (contoh: 'Orang: UPS'). Atribut adalah metadata yang dapat dicari dan disimpan secara terpisah di dalam metadata objek.",
|
||||
"classificationSubLabel": "Sub Label",
|
||||
"classificationAttribute": "Atribut",
|
||||
"classes": "Class",
|
||||
"classesTip": "Pelajari tentang class",
|
||||
"classesStateDesc": "Tentukan berbagai status (state) pada area kamera Anda. Contoh: 'terbuka' dan 'tertutup' untuk pintu garasi.",
|
||||
"classesObjectDesc": "Tentukan kategori berbeda untuk mengklasifikasikan objek yang terdeteksi. Contoh: 'kurir', 'penghuni', 'orang_asing' untuk klasifikasi orang.",
|
||||
"classPlaceholder": "Masukkan nama class...",
|
||||
"errors": {
|
||||
"nameRequired": "Nama model wajib diisi",
|
||||
"nameLength": "Nama model maksimal 64 karakter",
|
||||
"nameOnlyNumbers": "Nama model tidak boleh hanya berisi angka",
|
||||
"classRequired": "Setidaknya harus ada 1 class yang diisi",
|
||||
"classesUnique": "Nama class harus unik",
|
||||
"stateRequiresTwoClasses": "State model memerlukan minimal 2 class",
|
||||
"objectLabelRequired": "Silakan pilih label objek",
|
||||
"objectTypeRequired": "Silakan pilih tipe klasifikasi"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -208,7 +208,8 @@
|
||||
"bg": "Български (Bulgaro)",
|
||||
"gl": "Galego (Galiziano)",
|
||||
"id": "Bahasa Indonesia (Indonesiano)",
|
||||
"ur": "اردو (Urdu)"
|
||||
"ur": "اردو (Urdu)",
|
||||
"hr": "Hrvatski (Croato)"
|
||||
},
|
||||
"darkMode": {
|
||||
"label": "Modalità scura",
|
||||
|
||||
@ -6,7 +6,8 @@
|
||||
"title": "Frigate si sta riavviando",
|
||||
"content": "Questa pagina si ricaricherà in {{countdown}} secondi.",
|
||||
"button": "Forza ricarica ora"
|
||||
}
|
||||
},
|
||||
"description": "Questo fermerà brevemente Frigate mentre si riavvia."
|
||||
},
|
||||
"explore": {
|
||||
"plus": {
|
||||
@ -84,7 +85,7 @@
|
||||
"label": "Mostra statistiche di trasmissione",
|
||||
"desc": "Abilita questa opzione per visualizzare le statistiche della trasmissione come sovrapposizione sul flusso della telecamera."
|
||||
},
|
||||
"debugView": "Visualizzazione debug",
|
||||
"debugView": "Vista correzioni",
|
||||
"restreaming": {
|
||||
"disabled": "La ritrasmissione non è abilitata per questa telecamera.",
|
||||
"desc": {
|
||||
|
||||
@ -3,7 +3,8 @@
|
||||
"description": {
|
||||
"addFace": "Aggiungi una nuova raccolta alla Libreria dei Volti caricando la tua prima immagine.",
|
||||
"placeholder": "Inserisci un nome per questa raccolta",
|
||||
"invalidName": "Nome non valido. I nomi possono contenere solo lettere, numeri, spazi, apostrofi, caratteri di sottolineatura e trattini."
|
||||
"invalidName": "Nome non valido. I nomi possono contenere solo lettere, numeri, spazi, apostrofi, caratteri di sottolineatura e trattini.",
|
||||
"nameCannotContainHash": "Il nome non può contenere #."
|
||||
},
|
||||
"details": {
|
||||
"confidence": "Fiducia",
|
||||
|
||||
@ -10,7 +10,7 @@
|
||||
"general": "Impostazioni interfaccia - Frigate",
|
||||
"frigatePlus": "Impostazioni Frigate+ - Frigate",
|
||||
"notifications": "Impostazioni di notifiche - Frigate",
|
||||
"enrichments": "Impostazioni Componenti Aggiuntivi - Frigate",
|
||||
"enrichments": "Impostazioni di miglioramento - Frigate",
|
||||
"cameraManagement": "Gestisci telecamere - Frigate",
|
||||
"cameraReview": "Impostazioni revisione telecamera - Frigate"
|
||||
},
|
||||
@ -87,9 +87,9 @@
|
||||
"desc": "Mostra un riquadro della regione di interesse inviata al rilevatore di oggetti"
|
||||
},
|
||||
"noObjects": "Nessun oggetto",
|
||||
"title": "Debug",
|
||||
"desc": "La vista di debug mostra in tempo reale gli oggetti tracciati e le relative statistiche. L'elenco degli oggetti mostra un riepilogo in differita degli oggetti rilevati.",
|
||||
"debugging": "Debugging",
|
||||
"title": "Correzioni",
|
||||
"desc": "La vista di correzione mostra una vista in tempo reale degli oggetti tracciati e delle relative statistiche. L'elenco degli oggetti mostra un riepilogo ritardato degli oggetti rilevati.",
|
||||
"debugging": "Correzioni",
|
||||
"objectList": "Elenco degli oggetti",
|
||||
"mask": {
|
||||
"desc": "Mostra i poligoni della maschera di movimento",
|
||||
@ -181,6 +181,11 @@
|
||||
},
|
||||
"error": {
|
||||
"mustBeFinished": "Prima di salvare, è necessario terminare il disegno del poligono."
|
||||
},
|
||||
"type": {
|
||||
"zone": "zona",
|
||||
"motion_mask": "maschera di movimento",
|
||||
"object_mask": "maschera di oggetto"
|
||||
}
|
||||
},
|
||||
"inertia": {
|
||||
@ -377,14 +382,14 @@
|
||||
"classification": "Classificazione",
|
||||
"cameras": "Impostazioni telecamera",
|
||||
"masksAndZones": "Maschere / Zone",
|
||||
"debug": "Debug",
|
||||
"debug": "Correzioni",
|
||||
"users": "Utenti",
|
||||
"frigateplus": "Frigate+",
|
||||
"enrichments": "Componenti Aggiuntivi",
|
||||
"enrichments": "Miglioramenti",
|
||||
"triggers": "Inneschi",
|
||||
"roles": "Ruoli",
|
||||
"cameraManagement": "Gestione",
|
||||
"cameraReview": "Revisione"
|
||||
"cameraReview": "Rivedi"
|
||||
},
|
||||
"users": {
|
||||
"dialog": {
|
||||
@ -432,7 +437,7 @@
|
||||
"hide": "Nascondi password",
|
||||
"requirements": {
|
||||
"title": "Requisiti password:",
|
||||
"length": "Almeno 8 caratteri",
|
||||
"length": "Almeno 12 caratteri",
|
||||
"uppercase": "Almeno una lettera maiuscola",
|
||||
"digit": "Almeno una cifra",
|
||||
"special": "Almeno un carattere speciale (!@#$%^&*(),.?\":{}|<>)"
|
||||
@ -510,7 +515,7 @@
|
||||
},
|
||||
"playAlertVideos": {
|
||||
"label": "Riproduci video di avvisi",
|
||||
"desc": "Per impostazione predefinita, gli avvisi recenti nella dashboard Live vengono riprodotti come piccoli video in loop. Disabilita questa opzione per mostrare solo un'immagine statica degli avvisi recenti su questo dispositivo/browser."
|
||||
"desc": "Per impostazione predefinita, gli avvisi recenti nella schermata dal vivo vengono riprodotti come brevi video in ciclo. Disattiva questa opzione per visualizzare solo un'immagine statica degli avvisi recenti su questo dispositivo/browser."
|
||||
},
|
||||
"title": "Schermata dal vivo",
|
||||
"displayCameraNames": {
|
||||
@ -534,7 +539,7 @@
|
||||
"clearAll": "Cancella tutte le impostazioni di trasmissione"
|
||||
},
|
||||
"recordingsViewer": {
|
||||
"title": "Visualizzatore di registrazioni",
|
||||
"title": "Visualizzatore registrazioni",
|
||||
"defaultPlaybackRate": {
|
||||
"label": "Velocità di riproduzione predefinita",
|
||||
"desc": "Velocità di riproduzione predefinita per la riproduzione delle registrazioni."
|
||||
@ -640,7 +645,7 @@
|
||||
"title": "Regolatore di rilevamento del movimento",
|
||||
"contourArea": {
|
||||
"title": "Area di contorno",
|
||||
"desc": "Il valore dell'area di contorno viene utilizzato per decidere quali gruppi di pixel modificati possono essere considerati movimento. <em>Predefinito: 10</em>"
|
||||
"desc": "Il valore dell'area del contorno viene utilizzato per decidere quali gruppi di pixel modificati sono considerati movimento. <em>Predefinito: 10</em>"
|
||||
},
|
||||
"Threshold": {
|
||||
"title": "Soglia",
|
||||
@ -708,10 +713,10 @@
|
||||
},
|
||||
"enrichments": {
|
||||
"toast": {
|
||||
"success": "Le impostazioni dei componenti aggiuntivi sono state salvate. Riavvia Frigate per applicare le modifiche.",
|
||||
"success": "Le impostazioni di miglioramento sono state salvate. Riavvia Frigate per applicare le modifiche.",
|
||||
"error": "Impossibile salvare le modifiche alla configurazione: {{errorMessage}}"
|
||||
},
|
||||
"title": "Impostazioni Componenti Aggiuntivi",
|
||||
"title": "Impostazioni di miglioramento",
|
||||
"semanticSearch": {
|
||||
"reindexNow": {
|
||||
"desc": "La reindicizzazione rigenererà gli incorporamenti per tutti gli oggetti tracciati. Questo processo viene eseguito in sottofondo e potrebbe impegnare al massimo la CPU e richiedere un tempo considerevole, a seconda del numero di oggetti tracciati.",
|
||||
@ -765,8 +770,8 @@
|
||||
"title": "Riconoscimento targhe",
|
||||
"readTheDocumentation": "Leggi la documentazione"
|
||||
},
|
||||
"unsavedChanges": "Modifiche alle impostazioni dei Componenti aggiuntivi non salvate",
|
||||
"restart_required": "Riavvio richiesto (impostazioni dei componenti aggiuntivi modificate)"
|
||||
"unsavedChanges": "Modifiche alle impostazioni di miglioramento non salvate",
|
||||
"restart_required": "Riavvio richiesto (impostazioni di miglioramento modificate)"
|
||||
},
|
||||
"triggers": {
|
||||
"documentTitle": "Inneschi",
|
||||
@ -1278,7 +1283,7 @@
|
||||
"backToSettings": "Torna alle impostazioni della telecamera",
|
||||
"streams": {
|
||||
"title": "Abilita/Disabilita telecamere",
|
||||
"desc": "Disattiva temporaneamente una telecamera fino al riavvio di Frigate. La disattivazione completa di una telecamera interrompe l'elaborazione dei flussi di questa telecamera da parte di Frigate. Rilevamento, registrazione e debug non saranno disponibili.<br /> <em>Nota: questa operazione non disattiva le ritrasmissioni di go2rtc.</em>"
|
||||
"desc": "Disattiva temporaneamente una telecamera fino al riavvio di Frigate. La disattivazione completa di una telecamera interrompe l'elaborazione dei flussi di questa telecamera da parte di Frigate. Rilevamento, registrazione e correzioni non saranno disponibili.<br /> <em>Nota: questa operazione non disattiva le ritrasmissioni di go2rtc.</em>"
|
||||
},
|
||||
"cameraConfig": {
|
||||
"add": "Aggiungi telecamera",
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
{
|
||||
"documentTitle": {
|
||||
"cameras": "Statistiche telecamere - Frigate",
|
||||
"enrichments": "Statistiche Componenti Aggiuntivi - Frigate",
|
||||
"enrichments": "Statistiche di miglioramento - Frigate",
|
||||
"storage": "Statistiche archiviazione - Frigate",
|
||||
"general": "Statistiche generali - Frigate",
|
||||
"logs": {
|
||||
@ -117,7 +117,7 @@
|
||||
"classification_speed": "Velocità di classificazione {{name}}",
|
||||
"classification_events_per_second": "Eventi di classificazione {{name}} al secondo"
|
||||
},
|
||||
"title": "Componenti Aggiuntivi",
|
||||
"title": "Miglioramenti",
|
||||
"infPerSecond": "Inferenze al secondo",
|
||||
"averageInf": "Tempo medio di inferenza"
|
||||
},
|
||||
|
||||
@ -97,7 +97,8 @@
|
||||
"show": "{{item}} を表示",
|
||||
"ID": "ID",
|
||||
"none": "なし",
|
||||
"all": "すべて"
|
||||
"all": "すべて",
|
||||
"other": "その他"
|
||||
},
|
||||
"button": {
|
||||
"apply": "適用",
|
||||
|
||||
@ -6,7 +6,8 @@
|
||||
"content": "このページは {{countdown}} 秒後に再読み込みされます。",
|
||||
"button": "今すぐ強制再読み込み"
|
||||
},
|
||||
"button": "再起動"
|
||||
"button": "再起動",
|
||||
"description": "再起動の間、Frigateが一時的に停止します。"
|
||||
},
|
||||
"explore": {
|
||||
"plus": {
|
||||
|
||||
@ -288,6 +288,11 @@
|
||||
},
|
||||
"error": {
|
||||
"mustBeFinished": "保存する前に多角形の作図を完了してください。"
|
||||
},
|
||||
"type": {
|
||||
"zone": "ゾーン",
|
||||
"motion_mask": "モーションマスク",
|
||||
"object_mask": "オブジェクトマスク"
|
||||
}
|
||||
}
|
||||
},
|
||||
@ -532,7 +537,7 @@
|
||||
"hide": "パスワードを非表示",
|
||||
"requirements": {
|
||||
"title": "パスワード要件:",
|
||||
"length": "8 文字以上",
|
||||
"length": "12文字以上",
|
||||
"uppercase": "大文字を 1 文字以上含める",
|
||||
"digit": "数字を 1 文字以上含める",
|
||||
"special": "少なくとも 1 つの特殊文字(!@#$%^&*(),.?”:{}|<>)が必要です"
|
||||
@ -1185,11 +1190,11 @@
|
||||
"title": "カメラレビュー設定",
|
||||
"object_descriptions": {
|
||||
"title": "生成AIによるオブジェクト説明",
|
||||
"desc": "このカメラに対する生成AIのオブジェクト説明を一時的に有効/無効にします。無効にすると、このカメラの追跡オブジェクトについてAI生成の説明は要求されません。"
|
||||
"desc": "Frigateが再起動するまで、このカメラの生成AIによる物体説明を一時的に有効/無効にします。無効にすると、このカメラで追跡された物体に対してAI生成の説明は生成されません。"
|
||||
},
|
||||
"review_descriptions": {
|
||||
"title": "生成AIによるレビュー説明",
|
||||
"desc": "このカメラに対する生成AIのレビュー説明を一時的に有効/無効にします。無効にすると、このカメラのレビュー項目についてAI生成の説明は要求されません。"
|
||||
"desc": "Frigateが再起動するまで、このカメラの生成AIによるレビュー説明を一時的に有効/無効にします。無効にすると、このカメラのレビュー項目に対してAI生成の説明は生成されません。"
|
||||
},
|
||||
"review": {
|
||||
"title": "レビュー",
|
||||
|
||||
@ -91,7 +91,8 @@
|
||||
"recording": "録画",
|
||||
"review_segment": "レビューセグメント",
|
||||
"audio_detector": "音声検知",
|
||||
"go2rtc": "go2rtc"
|
||||
"go2rtc": "go2rtc",
|
||||
"embeddings": "ベクトル埋め込み"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
1
web/public/locales/ka/audio.json
Normal file
1
web/public/locales/ka/audio.json
Normal file
@ -0,0 +1 @@
|
||||
{}
|
||||
1
web/public/locales/ka/common.json
Normal file
1
web/public/locales/ka/common.json
Normal file
@ -0,0 +1 @@
|
||||
{}
|
||||
1
web/public/locales/ka/components/auth.json
Normal file
1
web/public/locales/ka/components/auth.json
Normal file
@ -0,0 +1 @@
|
||||
{}
|
||||
1
web/public/locales/ka/components/camera.json
Normal file
1
web/public/locales/ka/components/camera.json
Normal file
@ -0,0 +1 @@
|
||||
{}
|
||||
1
web/public/locales/ka/components/dialog.json
Normal file
1
web/public/locales/ka/components/dialog.json
Normal file
@ -0,0 +1 @@
|
||||
{}
|
||||
1
web/public/locales/ka/components/filter.json
Normal file
1
web/public/locales/ka/components/filter.json
Normal file
@ -0,0 +1 @@
|
||||
{}
|
||||
1
web/public/locales/ka/components/icons.json
Normal file
1
web/public/locales/ka/components/icons.json
Normal file
@ -0,0 +1 @@
|
||||
{}
|
||||
1
web/public/locales/ka/components/input.json
Normal file
1
web/public/locales/ka/components/input.json
Normal file
@ -0,0 +1 @@
|
||||
{}
|
||||
1
web/public/locales/ka/components/player.json
Normal file
1
web/public/locales/ka/components/player.json
Normal file
@ -0,0 +1 @@
|
||||
{}
|
||||
1
web/public/locales/ka/objects.json
Normal file
1
web/public/locales/ka/objects.json
Normal file
@ -0,0 +1 @@
|
||||
{}
|
||||
1
web/public/locales/ka/views/classificationModel.json
Normal file
1
web/public/locales/ka/views/classificationModel.json
Normal file
@ -0,0 +1 @@
|
||||
{}
|
||||
1
web/public/locales/ka/views/configEditor.json
Normal file
1
web/public/locales/ka/views/configEditor.json
Normal file
@ -0,0 +1 @@
|
||||
{}
|
||||
1
web/public/locales/ka/views/events.json
Normal file
1
web/public/locales/ka/views/events.json
Normal file
@ -0,0 +1 @@
|
||||
{}
|
||||
1
web/public/locales/ka/views/explore.json
Normal file
1
web/public/locales/ka/views/explore.json
Normal file
@ -0,0 +1 @@
|
||||
{}
|
||||
1
web/public/locales/ka/views/exports.json
Normal file
1
web/public/locales/ka/views/exports.json
Normal file
@ -0,0 +1 @@
|
||||
{}
|
||||
1
web/public/locales/ka/views/faceLibrary.json
Normal file
1
web/public/locales/ka/views/faceLibrary.json
Normal file
@ -0,0 +1 @@
|
||||
{}
|
||||
1
web/public/locales/ka/views/live.json
Normal file
1
web/public/locales/ka/views/live.json
Normal file
@ -0,0 +1 @@
|
||||
{}
|
||||
1
web/public/locales/ka/views/recording.json
Normal file
1
web/public/locales/ka/views/recording.json
Normal file
@ -0,0 +1 @@
|
||||
{}
|
||||
1
web/public/locales/ka/views/search.json
Normal file
1
web/public/locales/ka/views/search.json
Normal file
@ -0,0 +1 @@
|
||||
{}
|
||||
1
web/public/locales/ka/views/settings.json
Normal file
1
web/public/locales/ka/views/settings.json
Normal file
@ -0,0 +1 @@
|
||||
{}
|
||||
1
web/public/locales/ka/views/system.json
Normal file
1
web/public/locales/ka/views/system.json
Normal file
@ -0,0 +1 @@
|
||||
{}
|
||||
@ -81,7 +81,8 @@
|
||||
"untilForRestart": "Līdz Frigate pārstartējas.",
|
||||
"untilRestart": "Līdz pārstartēšanai",
|
||||
"ago": "{{timeAgo}} pirms",
|
||||
"justNow": "Nupat"
|
||||
"justNow": "Nupat",
|
||||
"never": "Nekad"
|
||||
},
|
||||
"unit": {
|
||||
"speed": {
|
||||
@ -107,7 +108,8 @@
|
||||
"show": "Rādīt {{item}}",
|
||||
"ID": "ID",
|
||||
"none": "Nav",
|
||||
"all": "Viss"
|
||||
"all": "Viss",
|
||||
"other": "Cits"
|
||||
},
|
||||
"list": {
|
||||
"two": "{{0}} un {{1}}",
|
||||
|
||||
@ -10,7 +10,8 @@
|
||||
"embeddingsReindexing": {
|
||||
"context": "Meklēšana būs pieejama pēc tam, kad būs pabeigta izsekoto objektu atkārtota indeksēšana.",
|
||||
"startingUp": "Notiek palaišana…",
|
||||
"estimatedTime": "Paredzamais atlikušais laiks:"
|
||||
"estimatedTime": "Paredzamais atlikušais laiks:",
|
||||
"finishingShortly": "Drīz pabeigs"
|
||||
}
|
||||
},
|
||||
"itemMenu": {
|
||||
|
||||
@ -2,7 +2,8 @@
|
||||
"description": {
|
||||
"addFace": "Pievienojiet savai seju bibliotēkai jaunu kolekciju, augšupielādējot savu pirmo attēlu.",
|
||||
"placeholder": "Ievadi kolekcijas nosaukumu",
|
||||
"invalidName": "Nederīgs nosaukums. Nosaukumi drīkst saturēt tikai burtus, ciparus, atstarpes, apostrofus, pasvītras un defises."
|
||||
"invalidName": "Nederīgs nosaukums. Nosaukumi drīkst saturēt tikai burtus, ciparus, atstarpes, apostrofus, pasvītras un defises.",
|
||||
"nameCannotContainHash": "Vārds nedrīkst saturēt #."
|
||||
},
|
||||
"details": {
|
||||
"timestamp": "Laika zīmogs",
|
||||
|
||||
@ -22,7 +22,161 @@
|
||||
"nameLength": "Kameras nosaukums nedrīkst būt garāks par 64 simboliem",
|
||||
"invalidCharacters": "Kameras nosaukumā ir neatļauti simboli",
|
||||
"nameExists": "Kameras nosaukums jau pastāv"
|
||||
},
|
||||
"onvifPort": "ONVIF Ports",
|
||||
"port": "Ports"
|
||||
},
|
||||
"title": "Pievienot Kameru",
|
||||
"testResultLabels": {
|
||||
"audio": "Audio",
|
||||
"video": "Video",
|
||||
"resolution": "Izšķirtspēja",
|
||||
"fps": "FSP"
|
||||
},
|
||||
"save": {
|
||||
"failure": "Kļūda saglabājot {{cameraName}}."
|
||||
},
|
||||
"steps": {
|
||||
"nameAndConnection": "Vārds un savienojums"
|
||||
},
|
||||
"step2": {
|
||||
"retry": "Atkārtot",
|
||||
"connected": "Savienots"
|
||||
},
|
||||
"step3": {
|
||||
"quality": "Kvalitāte",
|
||||
"resolution": "Izšķirtspēja",
|
||||
"selectQuality": "Izvēlies kvalitāti",
|
||||
"roleLabels": {
|
||||
"audio": "Audio"
|
||||
},
|
||||
"testStream": "Pārbaudīt Savienojumu",
|
||||
"connected": "Savienots",
|
||||
"notConnected": "Nav Savienots",
|
||||
"testFailedTitle": "Tests Neizdevās"
|
||||
},
|
||||
"step4": {
|
||||
"connectStream": "Savienot",
|
||||
"connectingStream": "Savienojas",
|
||||
"failed": "Neizdevās",
|
||||
"roles": "Lomas",
|
||||
"error": "Kļūda"
|
||||
}
|
||||
},
|
||||
"menu": {
|
||||
"users": "Lietotāji",
|
||||
"roles": "Lomas",
|
||||
"frigateplus": "Frigate+",
|
||||
"notifications": "Paziņojumi",
|
||||
"triggers": "Trigeri"
|
||||
},
|
||||
"cameraSetting": {
|
||||
"camera": "Kamera"
|
||||
},
|
||||
"dialog": {
|
||||
"unsavedChanges": {
|
||||
"title": "Tev ir nesaglabātas izmaiņas.",
|
||||
"desc": "Vai vēlies saglabāt izmaiņas pirms turpini?"
|
||||
}
|
||||
},
|
||||
"general": {
|
||||
"liveDashboard": {
|
||||
"displayCameraNames": {
|
||||
"label": "Vienmēr rādīt kameras nosaukumus"
|
||||
}
|
||||
},
|
||||
"calendar": {
|
||||
"title": "Kalendārs",
|
||||
"firstWeekday": {
|
||||
"label": "Nedēļas pirmā diena",
|
||||
"sunday": "Svētdiena",
|
||||
"monday": "Pirmdiena"
|
||||
}
|
||||
}
|
||||
},
|
||||
"enrichments": {
|
||||
"semanticSearch": {
|
||||
"reindexNow": {
|
||||
"confirmButton": "Pārindeksēt",
|
||||
"label": "Pārindeksēt tagad",
|
||||
"confirmTitle": "Apstiprināt Pārindeksāciju",
|
||||
"alreadyInProgress": "Pārindeksācija jau notiek."
|
||||
},
|
||||
"modelSize": {
|
||||
"small": {
|
||||
"title": "mazs"
|
||||
},
|
||||
"large": {
|
||||
"title": "liels"
|
||||
},
|
||||
"label": "Modeļa izmērs"
|
||||
}
|
||||
},
|
||||
"birdClassification": {
|
||||
"title": "Putnu klasifikācija"
|
||||
},
|
||||
"faceRecognition": {
|
||||
"title": "Sejas Atpazīšana",
|
||||
"modelSize": {
|
||||
"label": "Modeļa izmērs",
|
||||
"small": {
|
||||
"title": "mazs"
|
||||
},
|
||||
"large": {
|
||||
"title": "liels"
|
||||
}
|
||||
}
|
||||
},
|
||||
"licensePlateRecognition": {
|
||||
"title": "Auto numura zīmes atpazīšana"
|
||||
},
|
||||
"toast": {
|
||||
"error": "Neizdevās saglabāt konfigurācijas izmaiņas: {{errorMessage}}"
|
||||
}
|
||||
},
|
||||
"cameraManagement": {
|
||||
"addCamera": "Pievienot Jaunu Kameru",
|
||||
"selectCamera": "Izvēlēties Kameru",
|
||||
"cameraConfig": {
|
||||
"add": "Pievienot Kameru",
|
||||
"edit": "Labot Kameru",
|
||||
"name": "Kameras Vārds"
|
||||
}
|
||||
},
|
||||
"triggers": {
|
||||
"wizard": {
|
||||
"steps": {
|
||||
"nameAndType": "Vārds un Tips"
|
||||
}
|
||||
},
|
||||
"dialog": {
|
||||
"form": {
|
||||
"name": {
|
||||
"title": "Vārds"
|
||||
}
|
||||
}
|
||||
},
|
||||
"table": {
|
||||
"edit": "Labot",
|
||||
"name": "Vārds",
|
||||
"type": "Tips",
|
||||
"content": "Saturs"
|
||||
}
|
||||
},
|
||||
"frigatePlus": {
|
||||
"modelInfo": {
|
||||
"cameras": "Kameras"
|
||||
},
|
||||
"snapshotConfig": {
|
||||
"table": {
|
||||
"camera": "Kamera"
|
||||
}
|
||||
}
|
||||
},
|
||||
"notification": {
|
||||
"title": "Paziņojumi",
|
||||
"cameras": {
|
||||
"title": "Kameras"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -25,5 +25,6 @@
|
||||
"object_description": "Objekta apraksts",
|
||||
"object_description_events_per_second": "Objekta apraksts"
|
||||
}
|
||||
}
|
||||
},
|
||||
"title": "Sistēma"
|
||||
}
|
||||
|
||||
@ -277,6 +277,11 @@
|
||||
},
|
||||
"error": {
|
||||
"mustBeFinished": "Tegningen av polygonet må fullføres før lagring."
|
||||
},
|
||||
"type": {
|
||||
"zone": "sone",
|
||||
"motion_mask": "bevegelsesmaske",
|
||||
"object_mask": "objektmaske"
|
||||
}
|
||||
},
|
||||
"inertia": {
|
||||
|
||||
@ -59,7 +59,7 @@
|
||||
"second_other": "{{time}} seconden",
|
||||
"formattedTimestampHourMinute": {
|
||||
"24hour": "HH:mm",
|
||||
"12hour": "HH:mm"
|
||||
"12hour": "h:mm aaa"
|
||||
},
|
||||
"formattedTimestampMonthDayYearHourMinute": {
|
||||
"12hour": "d MMM yyyy, HH:mm",
|
||||
@ -71,7 +71,7 @@
|
||||
"24hour": "dd-MM-yy-HH-mm-ss"
|
||||
},
|
||||
"formattedTimestampHourMinuteSecond": {
|
||||
"12hour": "HH:mm:ss",
|
||||
"12hour": "h:mm:ss aaa",
|
||||
"24hour": "HH:mm:ss"
|
||||
},
|
||||
"formattedTimestampMonthDayHourMinute": {
|
||||
@ -202,7 +202,8 @@
|
||||
"bg": "Български (Bulgaars)",
|
||||
"gl": "Galego (Galicisch)",
|
||||
"id": "Bahasa Indonesia (Indonesisch)",
|
||||
"ur": "اردو (Urdu)"
|
||||
"ur": "اردو (Urdu)",
|
||||
"hr": "Hrvatski (Kroatisch)"
|
||||
},
|
||||
"darkMode": {
|
||||
"label": "Donkere modus",
|
||||
|
||||
@ -6,7 +6,8 @@
|
||||
"title": "Frigate wordt opnieuw gestart",
|
||||
"button": "Forceer herladen nu",
|
||||
"content": "Deze pagina zal herladen in {{countdown}} seconden."
|
||||
}
|
||||
},
|
||||
"description": "Dit zal Frigate kort stoppen terwijl het opnieuw opstart."
|
||||
},
|
||||
"explore": {
|
||||
"plus": {
|
||||
|
||||
@ -14,7 +14,8 @@
|
||||
"description": {
|
||||
"placeholder": "Voer een naam in voor deze verzameling",
|
||||
"addFace": "Voeg een nieuwe collectie toe aan de gezichtenbibliotheek door je eerste afbeelding te uploaden.",
|
||||
"invalidName": "Ongeldige naam. Namen mogen alleen letters, cijfers, spaties, apostroffen, underscores en koppeltekens bevatten."
|
||||
"invalidName": "Ongeldige naam. Namen mogen alleen letters, cijfers, spaties, apostroffen, underscores en koppeltekens bevatten.",
|
||||
"nameCannotContainHash": "De naam mag geen # bevatten."
|
||||
},
|
||||
"train": {
|
||||
"title": "Recente herkenningen",
|
||||
|
||||
@ -287,6 +287,11 @@
|
||||
},
|
||||
"reset": {
|
||||
"label": "Alle punten wissen"
|
||||
},
|
||||
"type": {
|
||||
"zone": "zone",
|
||||
"motion_mask": "bewegingsmasker",
|
||||
"object_mask": "objectmasker"
|
||||
}
|
||||
},
|
||||
"speed": {
|
||||
@ -546,7 +551,7 @@
|
||||
"hide": "Wachtwoord verbergen",
|
||||
"requirements": {
|
||||
"title": "Wachtwoordvereisten:",
|
||||
"length": "Minimaal 8 tekens",
|
||||
"length": "Minimaal 12 tekens",
|
||||
"uppercase": "Minimaal één hoofdletter",
|
||||
"digit": "Minimaal één cijfer",
|
||||
"special": "Minimaal één speciaal teken (!@#$%^&*(),.?\":{}|<>)"
|
||||
|
||||
@ -446,8 +446,8 @@
|
||||
"outside": "Na zewnątrz",
|
||||
"chird": "Child",
|
||||
"change_ringing": "Zmienny dzwonek",
|
||||
"shofar": "Shofar",
|
||||
"trickle": "Trickle",
|
||||
"shofar": "Szofar",
|
||||
"trickle": "Spływanie",
|
||||
"gush": "Wylew",
|
||||
"fill": "Napełnianie",
|
||||
"sonar": "Sonar",
|
||||
|
||||
@ -6,7 +6,8 @@
|
||||
"title": "Frigate uruchamia się ponownie",
|
||||
"content": "Strona odświeży się za {{countdown}} sekund.",
|
||||
"button": "Wymuś odświeżenie"
|
||||
}
|
||||
},
|
||||
"description": "Spowoduje to chwilowe zatrzymanie Frigate i ponowne uruchomienie."
|
||||
},
|
||||
"explore": {
|
||||
"plus": {
|
||||
|
||||
@ -278,6 +278,11 @@
|
||||
},
|
||||
"error": {
|
||||
"mustBeFinished": "Rysowanie wielokąta musi być zakończone przed zapisaniem."
|
||||
},
|
||||
"type": {
|
||||
"object_mask": "maska obiektowa",
|
||||
"motion_mask": "maska ruchu",
|
||||
"zone": "strefa"
|
||||
}
|
||||
},
|
||||
"speed": {
|
||||
@ -787,9 +792,9 @@
|
||||
"createRole": "Utworzono rolę {{role}}",
|
||||
"updateCameras": "Zaktualizowano kamery dla roli {{role}}",
|
||||
"deleteRole": "Rola {{role}} została usunięta",
|
||||
"userRolesUpdated_one": "{{count}} użytkowników przypisanych do tej roli zostało zaktualizowanych do roli 'viewer', która ma dostęp do wszystkich kamer.",
|
||||
"userRolesUpdated_few": "",
|
||||
"userRolesUpdated_many": ""
|
||||
"userRolesUpdated_one": "{{count}} użytkownik przypisany do tej roli został zaktualizowany do roli 'viewer', która ma dostęp do wszystkich kamer.",
|
||||
"userRolesUpdated_few": "{{count}} użytkowników przypisanych do tej roli zostało zaktualizowanych do roli 'viewer', która ma dostęp do wszystkich kamer.",
|
||||
"userRolesUpdated_many": "{{count}} użytkowników przypisanych do tej roli zostało zaktualizowanych do roli 'viewer', która ma dostęp do wszystkich kamer."
|
||||
},
|
||||
"error": {
|
||||
"createRoleFailed": "Nie udało się utworzyć roli: {{errorMessage}}",
|
||||
|
||||
@ -6,7 +6,8 @@
|
||||
"title": "Frigate repornește",
|
||||
"content": "Această pagină se va reâncărca automat în {{countdown}} secunde.",
|
||||
"button": "Forțează acum reîncărcarea"
|
||||
}
|
||||
},
|
||||
"description": "Acest lucru va opri temporar Frigate în timpul repornirii."
|
||||
},
|
||||
"explore": {
|
||||
"plus": {
|
||||
|
||||
@ -382,6 +382,11 @@
|
||||
},
|
||||
"error": {
|
||||
"mustBeFinished": "Desenul poligonului trebuie finalizat înainte de salvare."
|
||||
},
|
||||
"type": {
|
||||
"zone": "zonă",
|
||||
"motion_mask": "mască de mișcare",
|
||||
"object_mask": "mască de obiect"
|
||||
}
|
||||
},
|
||||
"distance": {
|
||||
|
||||
@ -231,7 +231,7 @@
|
||||
"music_of_asia": "Ázijská hudba",
|
||||
"carnatic_music": "Karnatická hudba",
|
||||
"music_of_bollywood": "Hudba z Bollywoodu",
|
||||
"ska": "SKA",
|
||||
"ska": "Ska",
|
||||
"traditional_music": "Tradičná hudba",
|
||||
"independent_music": "Nezávislá hudba",
|
||||
"song": "Pieseň",
|
||||
|
||||
@ -81,7 +81,8 @@
|
||||
},
|
||||
"inProgress": "Spracováva sa",
|
||||
"invalidStartTime": "Neplatný čas štartu",
|
||||
"invalidEndTime": "Neplatný čas ukončenia"
|
||||
"invalidEndTime": "Neplatný čas ukončenia",
|
||||
"never": "Nikdy"
|
||||
},
|
||||
"selectItem": "Vyberte {{item}}",
|
||||
"unit": {
|
||||
@ -98,8 +99,8 @@
|
||||
"mbps": "MB/s",
|
||||
"gbps": "GB/s",
|
||||
"kbph": "kb/hour",
|
||||
"mbph": "MB/hour",
|
||||
"gbph": "GB/hour"
|
||||
"mbph": "MB/hodinu",
|
||||
"gbph": "GB/hodinu"
|
||||
}
|
||||
},
|
||||
"readTheDocumentation": "Prečítajte si dokumentáciu",
|
||||
@ -109,7 +110,8 @@
|
||||
"show": "Zobraziť {{item}}",
|
||||
"ID": "ID",
|
||||
"none": "None",
|
||||
"all": "Všetko"
|
||||
"all": "Všetko",
|
||||
"other": "Iné"
|
||||
},
|
||||
"button": {
|
||||
"apply": "Použiť",
|
||||
@ -199,7 +201,8 @@
|
||||
"ur": "اردو (Urdu)",
|
||||
"withSystem": {
|
||||
"label": "Použiť systémové nastavenia pre jazyk"
|
||||
}
|
||||
},
|
||||
"hr": "Hrvatski (Croatian)"
|
||||
},
|
||||
"restart": "Reštartovať Frigate",
|
||||
"live": {
|
||||
|
||||
@ -128,9 +128,13 @@
|
||||
"loadFailed": "Nepodarilo sa načítať rozpoznané evidenčné čísla vozidiel.",
|
||||
"loading": "Načítavajú sa rozpoznané evidenčné čísla…",
|
||||
"placeholder": "Zadajte text pre vyhľadávanie evidenčných čísel…",
|
||||
"noLicensePlatesFound": "Neboli nájdené SPZ.",
|
||||
"noLicensePlatesFound": "Neboli nájdené evidenčné čísla vozidiel.",
|
||||
"selectPlatesFromList": "Vyberte jeden alebo viacero tanierov zo zoznamu.",
|
||||
"selectAll": "Vybrať všetko",
|
||||
"clearAll": "Vymazať všetko"
|
||||
},
|
||||
"attributes": {
|
||||
"label": "Klasifikačné Atribúty",
|
||||
"all": "Všetky Atribúty"
|
||||
}
|
||||
}
|
||||
|
||||
@ -102,7 +102,7 @@
|
||||
"waste_bin": "Odpadkový kôš",
|
||||
"on_demand": "Na požiadanie",
|
||||
"face": "Tvár",
|
||||
"license_plate": "ŠPZ",
|
||||
"license_plate": "Evidenčné Číslo Vozidla",
|
||||
"package": "Balíček",
|
||||
"bbq_grill": "Gril",
|
||||
"amazon": "Amazon",
|
||||
|
||||
@ -1,55 +1,59 @@
|
||||
{
|
||||
"documentTitle": "Klasifikačné modely",
|
||||
"documentTitle": "Klasifikačné modely - Frigate",
|
||||
"button": {
|
||||
"deleteClassificationAttempts": "Odstrániť obrázky klasifikácie",
|
||||
"renameCategory": "Premenovať triedu",
|
||||
"deleteCategory": "Odstrániť triedu",
|
||||
"deleteImages": "Odstrániť obrázky",
|
||||
"trainModel": "Model vlaku",
|
||||
"addClassification": "Pridať klasifikáciu",
|
||||
"deleteModels": "Odstrániť modely",
|
||||
"editModel": "Editovať model"
|
||||
"deleteClassificationAttempts": "Odstrániť Obrázky Klasifikácie",
|
||||
"renameCategory": "Premenovať Triedu",
|
||||
"deleteCategory": "Odstrániť Triedu",
|
||||
"deleteImages": "Odstrániť Obrázky",
|
||||
"trainModel": "Trénovať Model",
|
||||
"addClassification": "Pridať Klasifikáciu",
|
||||
"deleteModels": "Odstrániť Modely",
|
||||
"editModel": "Upraviť Model"
|
||||
},
|
||||
"toast": {
|
||||
"success": {
|
||||
"deletedCategory": "Vymazaná trieda",
|
||||
"deletedImage": "Vymazané obrázky",
|
||||
"deletedCategory": "Vymazaná Trieda",
|
||||
"deletedImage": "Vymazané Obrázky",
|
||||
"categorizedImage": "Obrázok bol úspešne klasifikovaný",
|
||||
"trainedModel": "Úspešne vyškolený model.",
|
||||
"trainingModel": "Úspešne spustený modelový tréning.",
|
||||
"deletedModel_one": "Úspešne zmazané {{count}} model (y)",
|
||||
"deletedModel_few": "",
|
||||
"deletedModel_other": "",
|
||||
"trainingModel": "Úspešne spustené trénovanie modelu.",
|
||||
"deletedModel_one": "Úspešne zmazaný {{count}} model",
|
||||
"deletedModel_few": "Úspešne zmazané {{count}} modely",
|
||||
"deletedModel_other": "Úspešne zmazaných {{count}} modelov",
|
||||
"updatedModel": "Úspešne zmenená konfigurácia modelu",
|
||||
"renamedCategory": "Úspešne premenovaná trieda na"
|
||||
"renamedCategory": "Úspešne premenovaná trieda na {{name}}"
|
||||
},
|
||||
"error": {
|
||||
"deleteImageFailed": "Nepodarilo sa odstrániť: {{errorMessage}}",
|
||||
"deleteCategoryFailed": "Nepodarilo sa odstrániť triedu: {{errorMessage}}",
|
||||
"categorizeFailed": "Nepodarilo sa kategorizovať obrázok: {{errorMessage}}",
|
||||
"trainingFailed": "Nepodarilo sa spustiť trénovanie modelu: {{errorMessage}}",
|
||||
"trainingFailed": "Trénovanie modelu zlyhalo. Skontroluj záznamy Frigate pre viac podrobností.",
|
||||
"deleteModelFailed": "Nepodarilo sa odstrániť model: {{errorMessage}}",
|
||||
"trainingFailedToStart": "Neuspešny štart trenovania modelu:",
|
||||
"updateModelFailed": "Chyba pri úprave modelu:",
|
||||
"renameCategoryFailed": "Chyba pri premenovani triedy:"
|
||||
"trainingFailedToStart": "Neuspešné spustenie trénovania modelu: {{errorMessage}}",
|
||||
"updateModelFailed": "Chyba pri aktualizácii modelu: {{errorMessage}}",
|
||||
"renameCategoryFailed": "Chyba pri premenovaní triedy: {{errorMessage}}"
|
||||
}
|
||||
},
|
||||
"deleteCategory": {
|
||||
"title": "Odstrániť triedu",
|
||||
"title": "Odstrániť Triedu",
|
||||
"desc": "Naozaj chcete odstrániť triedu {{name}}? Týmto sa natrvalo odstránia všetky súvisiace obrázky a bude potrebné pretrénovať model.",
|
||||
"minClassesTitle": "Nemožete zmazať triedu",
|
||||
"minClassesDesc": "Klasifikačný model musí mať aspoň 2 triedy. Pred odstránením tejto triedy pridajte ďalšiu triedu."
|
||||
},
|
||||
"deleteDatasetImages": {
|
||||
"title": "Odstrániť obrázky množiny údajov",
|
||||
"desc": "Naozaj chcete odstrániť {{count}} obrázkov z {{dataset}}? Túto akciu nie je možné vrátiť späť a bude si vyžadovať pretrénovanie modelu."
|
||||
"desc_one": "Naozaj chcete odstrániť {{count}} obrázok z {{dataset}}? Túto akciu nie je možné vrátiť späť a bude si vyžadovať pretrénovanie modelu.",
|
||||
"desc_few": "Naozaj chcete odstrániť {{count}} obrázky z {{dataset}}? Túto akciu nie je možné vrátiť späť a bude si vyžadovať pretrénovanie modelu.",
|
||||
"desc_other": "Naozaj chcete odstrániť {{count}} obrázkov z {{dataset}}? Túto akciu nie je možné vrátiť späť a bude si vyžadovať pretrénovanie modelu."
|
||||
},
|
||||
"deleteTrainImages": {
|
||||
"title": "Odstrániť obrázky vlakov",
|
||||
"desc": "Naozaj chcete odstrániť {{count}} obrázkov? Túto akciu nie je možné vrátiť späť."
|
||||
"title": "Odstrániť Trénovacie Obrázky",
|
||||
"desc_one": "Naozaj chcete odstrániť {{count}} obrázok? Túto akciu nie je možné vrátiť späť.",
|
||||
"desc_few": "Naozaj chcete odstrániť {{count}} obrázky? Túto akciu nie je možné vrátiť späť.",
|
||||
"desc_other": "Naozaj chcete odstrániť {{count}} obrázkov? Túto akciu nie je možné vrátiť späť."
|
||||
},
|
||||
"renameCategory": {
|
||||
"title": "Premenovať triedu",
|
||||
"title": "Premenovať Triedu",
|
||||
"desc": "Zadajte nový názov pre {{name}}. Budete musieť model pretrénovať, aby sa zmena názvu prejavila."
|
||||
},
|
||||
"description": {
|
||||
@ -112,7 +116,8 @@
|
||||
"classesUnique": "Názvy tried musia byť jedinečné",
|
||||
"stateRequiresTwoClasses": "Modely štátov vyžadujú aspoň 2 triedy",
|
||||
"objectLabelRequired": "Vyberte označenie objektu",
|
||||
"objectTypeRequired": "Vyberte typ klasifikácie"
|
||||
"objectTypeRequired": "Vyberte typ klasifikácie",
|
||||
"noneNotAllowed": "Trieda 'none' nie je povolená"
|
||||
},
|
||||
"states": "Štátov"
|
||||
},
|
||||
@ -151,32 +156,37 @@
|
||||
"allImagesRequired_other": "Uveďte všetky obrázky. {{count}} obrázkov zostávajú.",
|
||||
"modelCreated": "Model vytvorený úspešne. Použite aktuálne klasifikácie na pridanie obrázkov pre chýbajúce stavy a nasledne dajte trénovať model.",
|
||||
"missingStatesWarning": {
|
||||
"title": "Chýbajúce príklady stavov"
|
||||
"title": "Chýbajúce príklady stavov",
|
||||
"description": "Odporúča sa vybrať príklady pre všetky stavy pre dosiahnutie najlepších výsledkov. Môžeš pokračovať bez zvolenia všetkých stavov, ale model nebude natrénovaný pokiaľ všetky stavy nemajú obrázky. Po pokračovaní použi náhľad Nedávne Klasifikácie na klasifikovanie obrázkov pre chýbajúce stavy, potom natrénuj model."
|
||||
}
|
||||
}
|
||||
},
|
||||
"deleteModel": {
|
||||
"title": "Odstrániť klasifikačný model",
|
||||
"single": "Ste si istí, že chcete odstrániť {{name}}? To bude trvalo odstrániť všetky súvisiace údaje vrátane obrázkov a vzdelávacích údajov. Táto akcia nemôže byť neporušená.",
|
||||
"desc": "Ste si istí, že chcete odstrániť {{count}} model (y)? To bude trvalo odstrániť všetky súvisiace údaje vrátane obrázkov a vzdelávacích údajov. Táto akcia nemôže byť neporušená."
|
||||
"desc_one": "Ste si istí, že chcete odstrániť {{count}} model? To bude trvalo odstrániť všetky súvisiace údaje vrátane obrázkov a trénovacích údajov. Táto akcia nemôže byť neporušená.",
|
||||
"desc_few": "Ste si istí, že chcete odstrániť {{count}} modely? To bude trvalo odstrániť všetky súvisiace údaje vrátane obrázkov a trénovacích údajov. Táto akcia nemôže byť neporušená.",
|
||||
"desc_other": "Ste si istí, že chcete odstrániť {{count}} modelov? To bude trvalo odstrániť všetky súvisiace údaje vrátane obrázkov a trénovacích údajov. Táto akcia nemôže byť neporušená."
|
||||
},
|
||||
"menu": {
|
||||
"objects": "Objekty",
|
||||
"states": "Štátov"
|
||||
},
|
||||
"details": {
|
||||
"scoreInfo": "Skóre predstavuje priemernú istotu klasifikácie naprieč detekciami tohoto objektu."
|
||||
"scoreInfo": "Skóre predstavuje priemernú istotu klasifikácie naprieč všetkými detekciami tohoto objektu.",
|
||||
"none": "Žiadny",
|
||||
"unknown": "Neznámy"
|
||||
},
|
||||
"tooltip": {
|
||||
"trainingInProgress": "Model sa aktuálne trénuje",
|
||||
"noNewImages": "Žiadne nové obrázky na trénovanie. Najskor klasifikuj nové obrazky do datasetu.",
|
||||
"noNewImages": "Žiadne nové obrázky na trénovanie. Najskôr klasifikuj nové obrazky do datasetu.",
|
||||
"noChanges": "Žiadne zmeny v datasete od posledného tréningu.",
|
||||
"modelNotReady": "Model nie je pripravený na trénovanie."
|
||||
"modelNotReady": "Model nie je pripravený na trénovanie"
|
||||
},
|
||||
"edit": {
|
||||
"title": "Nastavenie modelu",
|
||||
"descriptionState": "Upravte triedy pre tento model klasifikácie. Zmeny budú vyžadovať pretrénovanie modelu.",
|
||||
"descriptionObject": "Upravte typ objektu a typ klasifikácie pre tento model klasifikácie.",
|
||||
"title": "Nastavenie Klasifikácie Modelu",
|
||||
"descriptionState": "Upravte triedy pre tento model stavovej klasifikácie. Zmeny budú vyžadovať pretrénovanie modelu.",
|
||||
"descriptionObject": "Upravte typ objektu a typ klasifikácie pre tento objektový model klasifikácie.",
|
||||
"stateClassesInfo": "Poznámka: Zmena tried stavov vyžaduje pretrénovanie modelu s aktualizovanými triedami."
|
||||
}
|
||||
}
|
||||
|
||||
@ -283,7 +283,7 @@
|
||||
"millisecondsToOffset": "Milisekundy na posunutie detekcie anotácií. <em>Predvolené: 0</em>",
|
||||
"tips": "TIP: Predstavte si klip udalosti, v ktorom osoba kráča zľava doprava. Ak je ohraničujúci rámček časovej osi udalosti stále naľavo od osoby, hodnota by sa mala znížiť. Podobne, ak osoba kráča zľava doprava a ohraničujúci rámček je stále pred ňou, hodnota by sa mala zvýšiť.",
|
||||
"toast": {
|
||||
"success": "Odsadenie anotácie pre {{camera}} bolo uložené do konfiguračného súboru. Reštartujte Frigate, aby sa zmeny prejavili."
|
||||
"success": "Odsadenie anotácie pre {{camera}} bolo uložené do konfiguračného súboru."
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user