mirror of
https://github.com/blakeblackshear/frigate.git
synced 2026-07-07 12:31:15 +03:00
Increase ruff coverage (#23644)
Some checks are pending
CI / AMD64 Build (push) Waiting to run
CI / ARM Build (push) Waiting to run
CI / Jetson Jetpack 6 (push) Waiting to run
CI / AMD64 Extra Build (push) Blocked by required conditions
CI / ARM Extra Build (push) Blocked by required conditions
CI / Synaptics Build (push) Blocked by required conditions
CI / Assemble and push default build (push) Blocked by required conditions
Some checks are pending
CI / AMD64 Build (push) Waiting to run
CI / ARM Build (push) Waiting to run
CI / Jetson Jetpack 6 (push) Waiting to run
CI / AMD64 Extra Build (push) Blocked by required conditions
CI / ARM Extra Build (push) Blocked by required conditions
CI / Synaptics Build (push) Blocked by required conditions
CI / Assemble and push default build (push) Blocked by required conditions
* Pin ruff * Add python upgrade fixes This enables python upgrade checks in ruff to look for deprecated types and patterns. This namely fixes: - usage of deprecated `Typing` which is now built in - some specific exceptions which are caught and have new aliases Some specific UP checks were also ignored as they are stylistic / unimportant and likely to cause bugs * Remove async blocking calls Use asyncio.to_thread on two remaining blocking calls to fix hanging event thread loop. Enable this specific rule to block it in the future. * Use proper logging mechanism * Correctly format logs * Raise with context When raising an exception include the from context to improve debugging * Cleanup
This commit is contained in:
parent
455b8687e8
commit
4ee12e6237
@ -1,4 +1,4 @@
|
|||||||
ruff
|
ruff == 0.15.20
|
||||||
|
|
||||||
# types
|
# types
|
||||||
types-peewee == 3.17.*
|
types-peewee == 3.17.*
|
||||||
|
|||||||
@ -11,10 +11,10 @@ except FileNotFoundError:
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
try:
|
try:
|
||||||
with open("/config/conv2rknn.yaml", "r") as config_file:
|
with open("/config/conv2rknn.yaml") as config_file:
|
||||||
configuration = yaml.safe_load(config_file)
|
configuration = yaml.safe_load(config_file)
|
||||||
except FileNotFoundError:
|
except FileNotFoundError:
|
||||||
raise Exception("Please place a config file at /config/conv2rknn.yaml")
|
raise Exception("Please place a config file at /config/conv2rknn.yaml") from None
|
||||||
|
|
||||||
if configuration["config"] != None:
|
if configuration["config"] != None:
|
||||||
rknn_config = configuration["config"]
|
rknn_config = configuration["config"]
|
||||||
@ -31,7 +31,7 @@ if "soc" not in configuration:
|
|||||||
with open("/proc/device-tree/compatible") as file:
|
with open("/proc/device-tree/compatible") as file:
|
||||||
soc = file.read().split(",")[-1].strip("\x00")
|
soc = file.read().split(",")[-1].strip("\x00")
|
||||||
except FileNotFoundError:
|
except FileNotFoundError:
|
||||||
raise Exception("Make sure to run docker in privileged mode.")
|
raise Exception("Make sure to run docker in privileged mode.") from None
|
||||||
|
|
||||||
configuration["soc"] = [
|
configuration["soc"] = [
|
||||||
soc,
|
soc,
|
||||||
|
|||||||
@ -4,7 +4,6 @@ import multiprocessing as mp
|
|||||||
import signal
|
import signal
|
||||||
import sys
|
import sys
|
||||||
import threading
|
import threading
|
||||||
from typing import Union
|
|
||||||
|
|
||||||
import ruamel.yaml
|
import ruamel.yaml
|
||||||
from pydantic import ValidationError
|
from pydantic import ValidationError
|
||||||
@ -54,7 +53,7 @@ def main() -> None:
|
|||||||
print("*************************************************************\n")
|
print("*************************************************************\n")
|
||||||
# Attempt to get the original config file for line number tracking
|
# Attempt to get the original config file for line number tracking
|
||||||
config_path = find_config_file()
|
config_path = find_config_file()
|
||||||
with open(config_path, "r") as f:
|
with open(config_path) as f:
|
||||||
yaml_config = ruamel.yaml.YAML()
|
yaml_config = ruamel.yaml.YAML()
|
||||||
yaml_config.preserve_quotes = True
|
yaml_config.preserve_quotes = True
|
||||||
full_config = yaml_config.load(f)
|
full_config = yaml_config.load(f)
|
||||||
@ -68,7 +67,7 @@ def main() -> None:
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
for i, part in enumerate(error_path):
|
for i, part in enumerate(error_path):
|
||||||
key: Union[int, str] = (
|
key: int | str = (
|
||||||
int(part) if isinstance(part, str) and part.isdigit() else part
|
int(part) if isinstance(part, str) and part.isdigit() else part
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@ -12,7 +12,7 @@ from datetime import datetime, timedelta
|
|||||||
from functools import reduce
|
from functools import reduce
|
||||||
from io import StringIO
|
from io import StringIO
|
||||||
from pathlib import Path as FilePath
|
from pathlib import Path as FilePath
|
||||||
from typing import Any, Dict, List, Optional
|
from typing import Any
|
||||||
|
|
||||||
import aiofiles
|
import aiofiles
|
||||||
import ruamel.yaml
|
import ruamel.yaml
|
||||||
@ -113,7 +113,7 @@ def version():
|
|||||||
@router.get("/stats", dependencies=[Depends(allow_any_authenticated())])
|
@router.get("/stats", dependencies=[Depends(allow_any_authenticated())])
|
||||||
def stats(
|
def stats(
|
||||||
request: Request,
|
request: Request,
|
||||||
allowed_cameras: List[str] = Depends(get_allowed_cameras_for_filter),
|
allowed_cameras: list[str] = Depends(get_allowed_cameras_for_filter),
|
||||||
):
|
):
|
||||||
stats_data = request.app.stats_emitter.get_latest_stats()
|
stats_data = request.app.stats_emitter.get_latest_stats()
|
||||||
|
|
||||||
@ -164,7 +164,7 @@ def metrics(request: Request):
|
|||||||
# Retrieve the latest statistics and update the Prometheus metrics
|
# Retrieve the latest statistics and update the Prometheus metrics
|
||||||
stats = request.app.stats_emitter.get_latest_stats()
|
stats = request.app.stats_emitter.get_latest_stats()
|
||||||
# query DB for count of events by camera, label
|
# query DB for count of events by camera, label
|
||||||
event_counts: List[Dict[str, Any]] = (
|
event_counts: list[dict[str, Any]] = (
|
||||||
Event.select(Event.camera, Event.label, fn.Count())
|
Event.select(Event.camera, Event.label, fn.Count())
|
||||||
.group_by(Event.camera, Event.label)
|
.group_by(Event.camera, Event.label)
|
||||||
.dicts()
|
.dicts()
|
||||||
@ -250,7 +250,7 @@ async def genai_probe(body: GenAIProbeBody):
|
|||||||
asyncio.to_thread(client.list_models),
|
asyncio.to_thread(client.list_models),
|
||||||
timeout=_PROBE_OUTER_TIMEOUT_SECONDS,
|
timeout=_PROBE_OUTER_TIMEOUT_SECONDS,
|
||||||
)
|
)
|
||||||
except asyncio.TimeoutError:
|
except TimeoutError:
|
||||||
return JSONResponse(
|
return JSONResponse(
|
||||||
content={"success": False, "message": "Probe timed out"},
|
content={"success": False, "message": "Probe timed out"},
|
||||||
)
|
)
|
||||||
@ -374,7 +374,7 @@ def config(request: Request):
|
|||||||
if model_path:
|
if model_path:
|
||||||
model_json_path = FilePath(model_path).with_suffix(".json")
|
model_json_path = FilePath(model_path).with_suffix(".json")
|
||||||
try:
|
try:
|
||||||
with open(model_json_path, "r") as f:
|
with open(model_json_path) as f:
|
||||||
model_plus_data = json.load(f)
|
model_plus_data = json.load(f)
|
||||||
config["model"]["plus"] = model_plus_data
|
config["model"]["plus"] = model_plus_data
|
||||||
except FileNotFoundError:
|
except FileNotFoundError:
|
||||||
@ -502,7 +502,7 @@ def config_raw():
|
|||||||
status_code=404,
|
status_code=404,
|
||||||
)
|
)
|
||||||
|
|
||||||
with open(config_file, "r") as f:
|
with open(config_file) as f:
|
||||||
raw_config = f.read()
|
raw_config = f.read()
|
||||||
f.close()
|
f.close()
|
||||||
|
|
||||||
@ -807,7 +807,7 @@ def config_set(request: Request, body: AppConfigSetBody):
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
with lock:
|
with lock:
|
||||||
with open(config_file, "r") as f:
|
with open(config_file) as f:
|
||||||
old_raw_config = f.read()
|
old_raw_config = f.read()
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@ -854,7 +854,7 @@ def config_set(request: Request, body: AppConfigSetBody):
|
|||||||
update_yaml_file_bulk(config_file, updates)
|
update_yaml_file_bulk(config_file, updates)
|
||||||
|
|
||||||
# validate the updated config
|
# validate the updated config
|
||||||
with open(config_file, "r") as f:
|
with open(config_file) as f:
|
||||||
new_raw_config = f.read()
|
new_raw_config = f.read()
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@ -1028,16 +1028,16 @@ def nvinfo():
|
|||||||
)
|
)
|
||||||
async def logs(
|
async def logs(
|
||||||
service: str = Path(enum=["frigate", "nginx", "go2rtc"]),
|
service: str = Path(enum=["frigate", "nginx", "go2rtc"]),
|
||||||
download: Optional[str] = None,
|
download: str | None = None,
|
||||||
stream: Optional[bool] = False,
|
stream: bool | None = False,
|
||||||
start: Optional[int] = 0,
|
start: int | None = 0,
|
||||||
end: Optional[int] = None,
|
end: int | None = None,
|
||||||
):
|
):
|
||||||
"""Get logs for the requested service (frigate/nginx/go2rtc)"""
|
"""Get logs for the requested service (frigate/nginx/go2rtc)"""
|
||||||
|
|
||||||
def download_logs(service_location: str):
|
def download_logs(service_location: str):
|
||||||
try:
|
try:
|
||||||
file = open(service_location, "r")
|
file = open(service_location)
|
||||||
contents = file.read()
|
contents = file.read()
|
||||||
file.close()
|
file.close()
|
||||||
return JSONResponse(jsonable_encoder(contents))
|
return JSONResponse(jsonable_encoder(contents))
|
||||||
@ -1052,7 +1052,7 @@ async def logs(
|
|||||||
"""Asynchronously stream log lines."""
|
"""Asynchronously stream log lines."""
|
||||||
buffer = ""
|
buffer = ""
|
||||||
try:
|
try:
|
||||||
async with aiofiles.open(file_path, "r") as file:
|
async with aiofiles.open(file_path) as file:
|
||||||
await file.seek(0, 2)
|
await file.seek(0, 2)
|
||||||
while True:
|
while True:
|
||||||
line = await file.readline()
|
line = await file.readline()
|
||||||
@ -1090,7 +1090,7 @@ async def logs(
|
|||||||
|
|
||||||
# For full logs initially
|
# For full logs initially
|
||||||
try:
|
try:
|
||||||
async with aiofiles.open(service_location, "r") as file:
|
async with aiofiles.open(service_location) as file:
|
||||||
contents = await file.read()
|
contents = await file.read()
|
||||||
|
|
||||||
total_lines, log_lines = process_logs(contents, service, start, end)
|
total_lines, log_lines = process_logs(contents, service, start, end)
|
||||||
@ -1231,7 +1231,7 @@ def get_media_sync_status(job_id: str):
|
|||||||
@router.get("/labels", dependencies=[Depends(allow_any_authenticated())])
|
@router.get("/labels", dependencies=[Depends(allow_any_authenticated())])
|
||||||
def get_labels(
|
def get_labels(
|
||||||
camera: str = "",
|
camera: str = "",
|
||||||
allowed_cameras: List[str] = Depends(get_allowed_cameras_for_filter),
|
allowed_cameras: list[str] = Depends(get_allowed_cameras_for_filter),
|
||||||
):
|
):
|
||||||
try:
|
try:
|
||||||
if camera:
|
if camera:
|
||||||
@ -1263,8 +1263,8 @@ def get_labels(
|
|||||||
|
|
||||||
@router.get("/sub_labels", dependencies=[Depends(allow_any_authenticated())])
|
@router.get("/sub_labels", dependencies=[Depends(allow_any_authenticated())])
|
||||||
def get_sub_labels(
|
def get_sub_labels(
|
||||||
split_joined: Optional[int] = None,
|
split_joined: int | None = None,
|
||||||
allowed_cameras: List[str] = Depends(get_allowed_cameras_for_filter),
|
allowed_cameras: list[str] = Depends(get_allowed_cameras_for_filter),
|
||||||
):
|
):
|
||||||
try:
|
try:
|
||||||
events = (
|
events = (
|
||||||
@ -1351,8 +1351,8 @@ def plusModels(request: Request, filterByCurrentModelDetector: bool = False):
|
|||||||
"/recognized_license_plates", dependencies=[Depends(allow_any_authenticated())]
|
"/recognized_license_plates", dependencies=[Depends(allow_any_authenticated())]
|
||||||
)
|
)
|
||||||
def get_recognized_license_plates(
|
def get_recognized_license_plates(
|
||||||
split_joined: Optional[int] = None,
|
split_joined: int | None = None,
|
||||||
allowed_cameras: List[str] = Depends(get_allowed_cameras_for_filter),
|
allowed_cameras: list[str] = Depends(get_allowed_cameras_for_filter),
|
||||||
):
|
):
|
||||||
try:
|
try:
|
||||||
query = (
|
query = (
|
||||||
@ -1393,8 +1393,8 @@ def get_recognized_license_plates(
|
|||||||
def timeline(
|
def timeline(
|
||||||
camera: str = "all",
|
camera: str = "all",
|
||||||
limit: int = 100,
|
limit: int = 100,
|
||||||
source_id: Optional[str] = None,
|
source_id: str | None = None,
|
||||||
allowed_cameras: List[str] = Depends(get_allowed_cameras_for_filter),
|
allowed_cameras: list[str] = Depends(get_allowed_cameras_for_filter),
|
||||||
):
|
):
|
||||||
clauses = []
|
clauses = []
|
||||||
|
|
||||||
@ -1408,20 +1408,20 @@ def timeline(
|
|||||||
]
|
]
|
||||||
|
|
||||||
if camera != "all":
|
if camera != "all":
|
||||||
clauses.append((Timeline.camera == camera))
|
clauses.append(Timeline.camera == camera)
|
||||||
|
|
||||||
if source_id:
|
if source_id:
|
||||||
source_ids = [sid.strip() for sid in source_id.split(",")]
|
source_ids = [sid.strip() for sid in source_id.split(",")]
|
||||||
if len(source_ids) == 1:
|
if len(source_ids) == 1:
|
||||||
clauses.append((Timeline.source_id == source_ids[0]))
|
clauses.append(Timeline.source_id == source_ids[0])
|
||||||
else:
|
else:
|
||||||
clauses.append((Timeline.source_id.in_(source_ids)))
|
clauses.append(Timeline.source_id.in_(source_ids))
|
||||||
|
|
||||||
# Enforce per-camera access control
|
# Enforce per-camera access control
|
||||||
clauses.append((Timeline.camera << allowed_cameras))
|
clauses.append(Timeline.camera << allowed_cameras)
|
||||||
|
|
||||||
if len(clauses) == 0:
|
if len(clauses) == 0:
|
||||||
clauses.append((True))
|
clauses.append(True)
|
||||||
|
|
||||||
timeline = (
|
timeline = (
|
||||||
Timeline.select(*selected_columns)
|
Timeline.select(*selected_columns)
|
||||||
@ -1437,7 +1437,7 @@ def timeline(
|
|||||||
@router.get("/timeline/hourly", dependencies=[Depends(allow_any_authenticated())])
|
@router.get("/timeline/hourly", dependencies=[Depends(allow_any_authenticated())])
|
||||||
def hourly_timeline(
|
def hourly_timeline(
|
||||||
params: AppTimelineHourlyQueryParameters = Depends(),
|
params: AppTimelineHourlyQueryParameters = Depends(),
|
||||||
allowed_cameras: List[str] = Depends(get_allowed_cameras_for_filter),
|
allowed_cameras: list[str] = Depends(get_allowed_cameras_for_filter),
|
||||||
):
|
):
|
||||||
"""Get hourly summary for timeline."""
|
"""Get hourly summary for timeline."""
|
||||||
cameras = params.cameras
|
cameras = params.cameras
|
||||||
@ -1454,23 +1454,23 @@ def hourly_timeline(
|
|||||||
|
|
||||||
if cameras != "all":
|
if cameras != "all":
|
||||||
camera_list = cameras.split(",")
|
camera_list = cameras.split(",")
|
||||||
clauses.append((Timeline.camera << camera_list))
|
clauses.append(Timeline.camera << camera_list)
|
||||||
|
|
||||||
# Enforce per-camera access control
|
# Enforce per-camera access control
|
||||||
clauses.append((Timeline.camera << allowed_cameras))
|
clauses.append(Timeline.camera << allowed_cameras)
|
||||||
|
|
||||||
if labels != "all":
|
if labels != "all":
|
||||||
label_list = labels.split(",")
|
label_list = labels.split(",")
|
||||||
clauses.append((Timeline.data["label"] << label_list))
|
clauses.append(Timeline.data["label"] << label_list)
|
||||||
|
|
||||||
if before:
|
if before:
|
||||||
clauses.append((Timeline.timestamp < before))
|
clauses.append(Timeline.timestamp < before)
|
||||||
|
|
||||||
if after:
|
if after:
|
||||||
clauses.append((Timeline.timestamp > after))
|
clauses.append(Timeline.timestamp > after)
|
||||||
|
|
||||||
if len(clauses) == 0:
|
if len(clauses) == 0:
|
||||||
clauses.append((True))
|
clauses.append(True)
|
||||||
|
|
||||||
timeline = (
|
timeline = (
|
||||||
Timeline.select(
|
Timeline.select(
|
||||||
|
|||||||
@ -11,7 +11,6 @@ import secrets
|
|||||||
import time
|
import time
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import List, Optional
|
|
||||||
from urllib.parse import parse_qs, urlparse
|
from urllib.parse import parse_qs, urlparse
|
||||||
|
|
||||||
from fastapi import APIRouter, Depends, HTTPException, Request, Response
|
from fastapi import APIRouter, Depends, HTTPException, Request, Response
|
||||||
@ -390,7 +389,7 @@ def verify_password(password, password_hash):
|
|||||||
return secrets.compare_digest(password_hash, compare_hash)
|
return secrets.compare_digest(password_hash, compare_hash)
|
||||||
|
|
||||||
|
|
||||||
def validate_password_strength(password: str) -> tuple[bool, Optional[str]]:
|
def validate_password_strength(password: str) -> tuple[bool, str | None]:
|
||||||
"""
|
"""
|
||||||
Validate password strength.
|
Validate password strength.
|
||||||
|
|
||||||
@ -445,7 +444,7 @@ async def get_current_user(request: Request):
|
|||||||
return {"username": username, "role": role}
|
return {"username": username, "role": role}
|
||||||
|
|
||||||
|
|
||||||
def require_role(required_roles: List[str]):
|
def require_role(required_roles: list[str]):
|
||||||
async def role_checker(request: Request):
|
async def role_checker(request: Request):
|
||||||
proxy_config: ProxyConfig = request.app.frigate_config.proxy
|
proxy_config: ProxyConfig = request.app.frigate_config.proxy
|
||||||
config_roles = list(request.app.frigate_config.auth.roles.keys())
|
config_roles = list(request.app.frigate_config.auth.roles.keys())
|
||||||
@ -1083,7 +1082,7 @@ async def update_role(
|
|||||||
|
|
||||||
|
|
||||||
async def require_camera_access(
|
async def require_camera_access(
|
||||||
camera_name: Optional[str] = None,
|
camera_name: str | None = None,
|
||||||
request: Request = None,
|
request: Request = None,
|
||||||
):
|
):
|
||||||
"""Dependency to enforce camera access based on user role."""
|
"""Dependency to enforce camera access based on user role."""
|
||||||
@ -1148,8 +1147,8 @@ GO2RTC_STREAM_PROXY_PATHS = frozenset(
|
|||||||
|
|
||||||
|
|
||||||
def deny_response_for_go2rtc_stream(
|
def deny_response_for_go2rtc_stream(
|
||||||
original_url: Optional[str], role: Optional[str], request: Request
|
original_url: str | None, role: str | None, request: Request
|
||||||
) -> Optional[int]:
|
) -> int | None:
|
||||||
"""Block role-restricted users from go2rtc live streams they cannot access.
|
"""Block role-restricted users from go2rtc live streams they cannot access.
|
||||||
|
|
||||||
Returns 403 when any `src` stream named in `original_url` resolves to a
|
Returns 403 when any `src` stream named in `original_url` resolves to a
|
||||||
@ -1194,7 +1193,7 @@ def deny_response_for_go2rtc_stream(
|
|||||||
|
|
||||||
|
|
||||||
async def require_go2rtc_stream_access(
|
async def require_go2rtc_stream_access(
|
||||||
stream_name: Optional[str] = None,
|
stream_name: str | None = None,
|
||||||
request: Request = None,
|
request: Request = None,
|
||||||
):
|
):
|
||||||
"""Dependency to enforce go2rtc stream access based on owning camera access."""
|
"""Dependency to enforce go2rtc stream access based on owning camera access."""
|
||||||
|
|||||||
@ -74,7 +74,7 @@ def _is_valid_host(host: str) -> bool:
|
|||||||
|
|
||||||
@router.get("/go2rtc/streams", dependencies=[Depends(allow_any_authenticated())])
|
@router.get("/go2rtc/streams", dependencies=[Depends(allow_any_authenticated())])
|
||||||
async def go2rtc_streams(request: Request):
|
async def go2rtc_streams(request: Request):
|
||||||
r = requests.get("http://127.0.0.1:1984/api/streams")
|
r = await asyncio.to_thread(requests.get, "http://127.0.0.1:1984/api/streams")
|
||||||
if not r.ok:
|
if not r.ok:
|
||||||
logger.error("Failed to fetch streams from go2rtc")
|
logger.error("Failed to fetch streams from go2rtc")
|
||||||
return JSONResponse(
|
return JSONResponse(
|
||||||
@ -1187,14 +1187,14 @@ async def delete_camera(
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
with lock:
|
with lock:
|
||||||
with open(config_file, "r") as f:
|
with open(config_file) as f:
|
||||||
old_raw_config = f.read()
|
old_raw_config = f.read()
|
||||||
|
|
||||||
try:
|
try:
|
||||||
yaml = YAML()
|
yaml = YAML()
|
||||||
yaml.indent(mapping=2, sequence=4, offset=2)
|
yaml.indent(mapping=2, sequence=4, offset=2)
|
||||||
|
|
||||||
with open(config_file, "r") as f:
|
with open(config_file) as f:
|
||||||
data = yaml.load(f)
|
data = yaml.load(f)
|
||||||
|
|
||||||
# Remove camera from config
|
# Remove camera from config
|
||||||
@ -1223,7 +1223,7 @@ async def delete_camera(
|
|||||||
with open(config_file, "w") as f:
|
with open(config_file, "w") as f:
|
||||||
yaml.dump(data, f)
|
yaml.dump(data, f)
|
||||||
|
|
||||||
with open(config_file, "r") as f:
|
with open(config_file) as f:
|
||||||
new_raw_config = f.read()
|
new_raw_config = f.read()
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@ -1285,7 +1285,8 @@ async def delete_camera(
|
|||||||
|
|
||||||
# Best-effort go2rtc stream removal
|
# Best-effort go2rtc stream removal
|
||||||
try:
|
try:
|
||||||
requests.delete(
|
await asyncio.to_thread(
|
||||||
|
requests.delete,
|
||||||
"http://127.0.0.1:1984/api/streams",
|
"http://127.0.0.1:1984/api/streams",
|
||||||
params={"src": camera_name},
|
params={"src": camera_name},
|
||||||
timeout=5,
|
timeout=5,
|
||||||
|
|||||||
@ -7,7 +7,7 @@ import operator
|
|||||||
import time
|
import time
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from functools import reduce
|
from functools import reduce
|
||||||
from typing import Any, Optional
|
from typing import Any
|
||||||
|
|
||||||
import cv2
|
import cv2
|
||||||
from fastapi import APIRouter, Body, Depends, HTTPException, Request
|
from fastapi import APIRouter, Body, Depends, HTTPException, Request
|
||||||
@ -201,7 +201,7 @@ async def _execute_search_objects(
|
|||||||
# Return it as-is for the LLM
|
# Return it as-is for the LLM
|
||||||
return response
|
return response
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Error executing search_objects: {e}", exc_info=True)
|
logger.exception(f"Error executing search_objects: {e}")
|
||||||
return JSONResponse(
|
return JSONResponse(
|
||||||
content={
|
content={
|
||||||
"success": False,
|
"success": False,
|
||||||
@ -611,7 +611,7 @@ async def _execute_get_live_context(
|
|||||||
return result
|
return result
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Error executing get_live_context: {e}", exc_info=True)
|
logger.exception(f"Error executing get_live_context: {e}")
|
||||||
return {
|
return {
|
||||||
"error": "Error getting live context",
|
"error": "Error getting live context",
|
||||||
}
|
}
|
||||||
@ -621,7 +621,7 @@ async def _get_live_frame_image_url(
|
|||||||
request: Request,
|
request: Request,
|
||||||
camera: str,
|
camera: str,
|
||||||
allowed_cameras: list[str],
|
allowed_cameras: list[str],
|
||||||
) -> Optional[str]:
|
) -> str | None:
|
||||||
"""
|
"""
|
||||||
Fetch the current live frame for a camera as a base64 data URL.
|
Fetch the current live frame for a camera as a base64 data URL.
|
||||||
|
|
||||||
@ -801,7 +801,7 @@ async def _execute_start_camera_watch(
|
|||||||
zones=zones,
|
zones=zones,
|
||||||
)
|
)
|
||||||
except RuntimeError as e:
|
except RuntimeError as e:
|
||||||
logger.error("Failed to start VLM watch job: %s", e, exc_info=True)
|
logger.exception("Failed to start VLM watch job: %s", e)
|
||||||
return {"error": "Failed to start VLM watch job."}
|
return {"error": "Failed to start VLM watch job."}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@ -979,7 +979,7 @@ def _execute_get_recap(
|
|||||||
|
|
||||||
return {"events": events}
|
return {"events": events}
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error("Error executing get_recap: %s", e, exc_info=True)
|
logger.exception("Error executing get_recap: %s", e)
|
||||||
return {"error": "Failed to fetch recap data."}
|
return {"error": "Failed to fetch recap data."}
|
||||||
|
|
||||||
|
|
||||||
@ -1072,13 +1072,12 @@ async def _execute_pending_tools(
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(
|
logger.exception(
|
||||||
"Error executing tool %s (id: %s): %s. Arguments: %s",
|
"Error executing tool %s (id: %s): %s. Arguments: %s",
|
||||||
tool_name,
|
tool_name,
|
||||||
tool_call_id,
|
tool_call_id,
|
||||||
e,
|
e,
|
||||||
json.dumps(tool_args),
|
json.dumps(tool_args),
|
||||||
exc_info=True,
|
|
||||||
)
|
)
|
||||||
error_content = json.dumps({"error": f"Tool execution failed: {str(e)}"})
|
error_content = json.dumps({"error": f"Tool execution failed: {str(e)}"})
|
||||||
tool_calls_out.append(
|
tool_calls_out.append(
|
||||||
@ -1186,7 +1185,7 @@ async def chat_completion(
|
|||||||
async def stream_body_llm():
|
async def stream_body_llm():
|
||||||
nonlocal conversation, stream_iterations
|
nonlocal conversation, stream_iterations
|
||||||
|
|
||||||
def _emit_chain(extra: Optional[list[dict[str, Any]]] = None):
|
def _emit_chain(extra: list[dict[str, Any]] | None = None):
|
||||||
# Return the full conversation (including the system message) so
|
# Return the full conversation (including the system message) so
|
||||||
# the client persists and replays it verbatim next turn.
|
# the client persists and replays it verbatim next turn.
|
||||||
chain = conversation + (extra or [])
|
chain = conversation + (extra or [])
|
||||||
@ -1414,7 +1413,7 @@ async def chat_completion(
|
|||||||
)
|
)
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Error in chat completion: {e}", exc_info=True)
|
logger.exception(f"Error in chat completion: {e}")
|
||||||
return JSONResponse(
|
return JSONResponse(
|
||||||
content={
|
content={
|
||||||
"error": "An error occurred while processing your request.",
|
"error": "An error occurred while processing your request.",
|
||||||
@ -1477,7 +1476,7 @@ async def start_vlm_monitor(
|
|||||||
username=request.headers.get("remote-user", ""),
|
username=request.headers.get("remote-user", ""),
|
||||||
)
|
)
|
||||||
except RuntimeError as e:
|
except RuntimeError as e:
|
||||||
logger.error("Failed to start VLM watch job: %s", e, exc_info=True)
|
logger.exception("Failed to start VLM watch job: %s", e)
|
||||||
return JSONResponse(
|
return JSONResponse(
|
||||||
content={"success": False, "message": "Failed to start VLM watch job."},
|
content={"success": False, "message": "Failed to start VLM watch job."},
|
||||||
status_code=409,
|
status_code=409,
|
||||||
|
|||||||
@ -9,8 +9,9 @@ loop state — all inputs and outputs are plain data.
|
|||||||
import logging
|
import logging
|
||||||
import math
|
import math
|
||||||
import time
|
import time
|
||||||
|
from collections.abc import Generator
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from typing import Any, Dict, Generator, List, Optional
|
from typing import Any
|
||||||
|
|
||||||
from frigate.embeddings.util import ZScoreNormalization
|
from frigate.embeddings.util import ZScoreNormalization
|
||||||
from frigate.models import Event
|
from frigate.models import Event
|
||||||
@ -30,7 +31,7 @@ def chunk_content(content: str, chunk_size: int = 80) -> Generator[str, None, No
|
|||||||
if not content:
|
if not content:
|
||||||
return
|
return
|
||||||
words = content.split(" ")
|
words = content.split(" ")
|
||||||
current: List[str] = []
|
current: list[str] = []
|
||||||
current_len = 0
|
current_len = 0
|
||||||
for w in words:
|
for w in words:
|
||||||
current.append(w)
|
current.append(w)
|
||||||
@ -44,8 +45,8 @@ def chunk_content(content: str, chunk_size: int = 80) -> Generator[str, None, No
|
|||||||
|
|
||||||
|
|
||||||
def format_events_with_local_time(
|
def format_events_with_local_time(
|
||||||
events_list: List[Dict[str, Any]],
|
events_list: list[dict[str, Any]],
|
||||||
) -> List[Dict[str, Any]]:
|
) -> list[dict[str, Any]]:
|
||||||
"""Add human-readable local start/end times to each event for the LLM."""
|
"""Add human-readable local start/end times to each event for the LLM."""
|
||||||
result = []
|
result = []
|
||||||
for evt in events_list:
|
for evt in events_list:
|
||||||
@ -84,9 +85,9 @@ def distance_to_score(distance: float, stats: ZScoreNormalization) -> float:
|
|||||||
|
|
||||||
|
|
||||||
def fuse_scores(
|
def fuse_scores(
|
||||||
visual_score: Optional[float],
|
visual_score: float | None,
|
||||||
description_score: Optional[float],
|
description_score: float | None,
|
||||||
) -> Optional[float]:
|
) -> float | None:
|
||||||
"""Weighted fusion of visual and description similarity scores.
|
"""Weighted fusion of visual and description similarity scores.
|
||||||
|
|
||||||
If one side is missing (e.g., no description embedding for this event),
|
If one side is missing (e.g., no description embedding for this event),
|
||||||
@ -102,7 +103,7 @@ def fuse_scores(
|
|||||||
return VISUAL_WEIGHT * visual_score + DESCRIPTION_WEIGHT * description_score
|
return VISUAL_WEIGHT * visual_score + DESCRIPTION_WEIGHT * description_score
|
||||||
|
|
||||||
|
|
||||||
def parse_iso_to_timestamp(value: Optional[str]) -> Optional[float]:
|
def parse_iso_to_timestamp(value: str | None) -> float | None:
|
||||||
"""Parse an ISO-8601 string as server-local time -> unix timestamp.
|
"""Parse an ISO-8601 string as server-local time -> unix timestamp.
|
||||||
|
|
||||||
Mirrors the parsing _execute_search_objects uses so both tools accept the
|
Mirrors the parsing _execute_search_objects uses so both tools accept the
|
||||||
@ -119,9 +120,9 @@ def parse_iso_to_timestamp(value: Optional[str]) -> Optional[float]:
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
def hydrate_event(event: Event, score: Optional[float] = None) -> Dict[str, Any]:
|
def hydrate_event(event: Event, score: float | None = None) -> dict[str, Any]:
|
||||||
"""Convert an Event row into the dict shape returned by find_similar_objects."""
|
"""Convert an Event row into the dict shape returned by find_similar_objects."""
|
||||||
data: Dict[str, Any] = {
|
data: dict[str, Any] = {
|
||||||
"id": event.id,
|
"id": event.id,
|
||||||
"camera": event.camera,
|
"camera": event.camera,
|
||||||
"label": event.label,
|
"label": event.label,
|
||||||
|
|||||||
@ -1,12 +1,10 @@
|
|||||||
from typing import Optional
|
|
||||||
|
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
|
|
||||||
|
|
||||||
class AppTimelineHourlyQueryParameters(BaseModel):
|
class AppTimelineHourlyQueryParameters(BaseModel):
|
||||||
cameras: Optional[str] = "all"
|
cameras: str | None = "all"
|
||||||
labels: Optional[str] = "all"
|
labels: str | None = "all"
|
||||||
after: Optional[float] = None
|
after: float | None = None
|
||||||
before: Optional[float] = None
|
before: float | None = None
|
||||||
limit: Optional[int] = 200
|
limit: int | None = 200
|
||||||
timezone: Optional[str] = "utc"
|
timezone: str | None = "utc"
|
||||||
|
|||||||
@ -1,28 +1,26 @@
|
|||||||
from typing import Optional
|
|
||||||
|
|
||||||
from pydantic import BaseModel, Field
|
from pydantic import BaseModel, Field
|
||||||
|
|
||||||
DEFAULT_TIME_RANGE = "00:00,24:00"
|
DEFAULT_TIME_RANGE = "00:00,24:00"
|
||||||
|
|
||||||
|
|
||||||
class EventsQueryParams(BaseModel):
|
class EventsQueryParams(BaseModel):
|
||||||
camera: Optional[str] = "all"
|
camera: str | None = "all"
|
||||||
cameras: Optional[str] = "all"
|
cameras: str | None = "all"
|
||||||
label: Optional[str] = "all"
|
label: str | None = "all"
|
||||||
labels: Optional[str] = "all"
|
labels: str | None = "all"
|
||||||
sub_label: Optional[str] = "all"
|
sub_label: str | None = "all"
|
||||||
sub_labels: Optional[str] = "all"
|
sub_labels: str | None = "all"
|
||||||
attributes: Optional[str] = "all"
|
attributes: str | None = "all"
|
||||||
zone: Optional[str] = "all"
|
zone: str | None = "all"
|
||||||
zones: Optional[str] = "all"
|
zones: str | None = "all"
|
||||||
limit: Optional[int] = 100
|
limit: int | None = 100
|
||||||
after: Optional[float] = None
|
after: float | None = None
|
||||||
before: Optional[float] = None
|
before: float | None = None
|
||||||
time_range: Optional[str] = DEFAULT_TIME_RANGE
|
time_range: str | None = DEFAULT_TIME_RANGE
|
||||||
has_clip: Optional[int] = None
|
has_clip: int | None = None
|
||||||
has_snapshot: Optional[int] = None
|
has_snapshot: int | None = None
|
||||||
in_progress: Optional[int] = None
|
in_progress: int | None = None
|
||||||
include_thumbnails: Optional[int] = Field(
|
include_thumbnails: int | None = Field(
|
||||||
1,
|
1,
|
||||||
description=(
|
description=(
|
||||||
"Deprecated. Thumbnail data is no longer included in the response. "
|
"Deprecated. Thumbnail data is no longer included in the response. "
|
||||||
@ -30,25 +28,25 @@ class EventsQueryParams(BaseModel):
|
|||||||
),
|
),
|
||||||
deprecated=True,
|
deprecated=True,
|
||||||
)
|
)
|
||||||
favorites: Optional[int] = None
|
favorites: int | None = None
|
||||||
min_score: Optional[float] = None
|
min_score: float | None = None
|
||||||
max_score: Optional[float] = None
|
max_score: float | None = None
|
||||||
min_speed: Optional[float] = None
|
min_speed: float | None = None
|
||||||
max_speed: Optional[float] = None
|
max_speed: float | None = None
|
||||||
recognized_license_plate: Optional[str] = "all"
|
recognized_license_plate: str | None = "all"
|
||||||
is_submitted: Optional[int] = None
|
is_submitted: int | None = None
|
||||||
min_length: Optional[float] = None
|
min_length: float | None = None
|
||||||
max_length: Optional[float] = None
|
max_length: float | None = None
|
||||||
event_id: Optional[str] = None
|
event_id: str | None = None
|
||||||
sort: Optional[str] = None
|
sort: str | None = None
|
||||||
timezone: Optional[str] = "utc"
|
timezone: str | None = "utc"
|
||||||
|
|
||||||
|
|
||||||
class EventsSearchQueryParams(BaseModel):
|
class EventsSearchQueryParams(BaseModel):
|
||||||
query: Optional[str] = None
|
query: str | None = None
|
||||||
event_id: Optional[str] = None
|
event_id: str | None = None
|
||||||
search_type: Optional[str] = "thumbnail"
|
search_type: str | None = "thumbnail"
|
||||||
include_thumbnails: Optional[int] = Field(
|
include_thumbnails: int | None = Field(
|
||||||
1,
|
1,
|
||||||
description=(
|
description=(
|
||||||
"Deprecated. Thumbnail data is no longer included in the response. "
|
"Deprecated. Thumbnail data is no longer included in the response. "
|
||||||
@ -56,28 +54,28 @@ class EventsSearchQueryParams(BaseModel):
|
|||||||
),
|
),
|
||||||
deprecated=True,
|
deprecated=True,
|
||||||
)
|
)
|
||||||
limit: Optional[int] = 50
|
limit: int | None = 50
|
||||||
cameras: Optional[str] = "all"
|
cameras: str | None = "all"
|
||||||
labels: Optional[str] = "all"
|
labels: str | None = "all"
|
||||||
sub_labels: Optional[str] = "all"
|
sub_labels: str | None = "all"
|
||||||
attributes: Optional[str] = "all"
|
attributes: str | None = "all"
|
||||||
zones: Optional[str] = "all"
|
zones: str | None = "all"
|
||||||
after: Optional[float] = None
|
after: float | None = None
|
||||||
before: Optional[float] = None
|
before: float | None = None
|
||||||
time_range: Optional[str] = DEFAULT_TIME_RANGE
|
time_range: str | None = DEFAULT_TIME_RANGE
|
||||||
has_clip: Optional[bool] = None
|
has_clip: bool | None = None
|
||||||
has_snapshot: Optional[bool] = None
|
has_snapshot: bool | None = None
|
||||||
is_submitted: Optional[bool] = None
|
is_submitted: bool | None = None
|
||||||
timezone: Optional[str] = "utc"
|
timezone: str | None = "utc"
|
||||||
min_score: Optional[float] = None
|
min_score: float | None = None
|
||||||
max_score: Optional[float] = None
|
max_score: float | None = None
|
||||||
min_speed: Optional[float] = None
|
min_speed: float | None = None
|
||||||
max_speed: Optional[float] = None
|
max_speed: float | None = None
|
||||||
recognized_license_plate: Optional[str] = "all"
|
recognized_license_plate: str | None = "all"
|
||||||
sort: Optional[str] = None
|
sort: str | None = None
|
||||||
|
|
||||||
|
|
||||||
class EventsSummaryQueryParams(BaseModel):
|
class EventsSummaryQueryParams(BaseModel):
|
||||||
timezone: Optional[str] = "utc"
|
timezone: str | None = "utc"
|
||||||
has_clip: Optional[int] = None
|
has_clip: int | None = None
|
||||||
has_snapshot: Optional[int] = None
|
has_snapshot: int | None = None
|
||||||
|
|||||||
@ -1,5 +1,4 @@
|
|||||||
from enum import Enum
|
from enum import Enum
|
||||||
from typing import Optional
|
|
||||||
|
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
|
|
||||||
@ -17,33 +16,33 @@ class Extension(str, Enum):
|
|||||||
|
|
||||||
|
|
||||||
class MediaLatestFrameQueryParams(BaseModel):
|
class MediaLatestFrameQueryParams(BaseModel):
|
||||||
bbox: Optional[int] = None
|
bbox: int | None = None
|
||||||
timestamp: Optional[int] = None
|
timestamp: int | None = None
|
||||||
zones: Optional[int] = None
|
zones: int | None = None
|
||||||
mask: Optional[int] = None
|
mask: int | None = None
|
||||||
motion: Optional[int] = None
|
motion: int | None = None
|
||||||
paths: Optional[int] = None
|
paths: int | None = None
|
||||||
regions: Optional[int] = None
|
regions: int | None = None
|
||||||
quality: Optional[int] = 70
|
quality: int | None = 70
|
||||||
height: Optional[int] = None
|
height: int | None = None
|
||||||
store: Optional[int] = None
|
store: int | None = None
|
||||||
|
|
||||||
|
|
||||||
class MediaEventsSnapshotQueryParams(BaseModel):
|
class MediaEventsSnapshotQueryParams(BaseModel):
|
||||||
download: Optional[bool] = False
|
download: bool | None = False
|
||||||
timestamp: Optional[int] = None
|
timestamp: int | None = None
|
||||||
bbox: Optional[int] = None
|
bbox: int | None = None
|
||||||
crop: Optional[int] = None
|
crop: int | None = None
|
||||||
height: Optional[int] = None
|
height: int | None = None
|
||||||
quality: Optional[int] = None
|
quality: int | None = None
|
||||||
|
|
||||||
|
|
||||||
class MediaMjpegFeedQueryParams(BaseModel):
|
class MediaMjpegFeedQueryParams(BaseModel):
|
||||||
fps: int = 3
|
fps: int = 3
|
||||||
height: int = 360
|
height: int = 360
|
||||||
bbox: Optional[int] = None
|
bbox: int | None = None
|
||||||
timestamp: Optional[int] = None
|
timestamp: int | None = None
|
||||||
zones: Optional[int] = None
|
zones: int | None = None
|
||||||
mask: Optional[int] = None
|
mask: int | None = None
|
||||||
motion: Optional[int] = None
|
motion: int | None = None
|
||||||
regions: Optional[int] = None
|
regions: int | None = None
|
||||||
|
|||||||
@ -1,21 +1,19 @@
|
|||||||
from typing import Optional, Union
|
|
||||||
|
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
from pydantic.json_schema import SkipJsonSchema
|
from pydantic.json_schema import SkipJsonSchema
|
||||||
|
|
||||||
|
|
||||||
class MediaRecordingsSummaryQueryParams(BaseModel):
|
class MediaRecordingsSummaryQueryParams(BaseModel):
|
||||||
timezone: str = "utc"
|
timezone: str = "utc"
|
||||||
cameras: Optional[str] = "all"
|
cameras: str | None = "all"
|
||||||
|
|
||||||
|
|
||||||
class MediaRecordingsAvailabilityQueryParams(BaseModel):
|
class MediaRecordingsAvailabilityQueryParams(BaseModel):
|
||||||
cameras: str = "all"
|
cameras: str = "all"
|
||||||
before: Union[float, SkipJsonSchema[None]] = None
|
before: float | SkipJsonSchema[None] = None
|
||||||
after: Union[float, SkipJsonSchema[None]] = None
|
after: float | SkipJsonSchema[None] = None
|
||||||
scale: int = 30
|
scale: int = 30
|
||||||
|
|
||||||
|
|
||||||
class RecordingsDeleteQueryParams(BaseModel):
|
class RecordingsDeleteQueryParams(BaseModel):
|
||||||
keep: Optional[str] = None
|
keep: str | None = None
|
||||||
cameras: Optional[str] = "all"
|
cameras: str | None = "all"
|
||||||
|
|||||||
@ -1,13 +1,11 @@
|
|||||||
from typing import Optional
|
|
||||||
|
|
||||||
from pydantic import BaseModel, Field
|
from pydantic import BaseModel, Field
|
||||||
|
|
||||||
from frigate.events.types import RegenerateDescriptionEnum
|
from frigate.events.types import RegenerateDescriptionEnum
|
||||||
|
|
||||||
|
|
||||||
class RegenerateQueryParameters(BaseModel):
|
class RegenerateQueryParameters(BaseModel):
|
||||||
source: Optional[RegenerateDescriptionEnum] = RegenerateDescriptionEnum.thumbnails
|
source: RegenerateDescriptionEnum | None = RegenerateDescriptionEnum.thumbnails
|
||||||
force: Optional[bool] = Field(
|
force: bool | None = Field(
|
||||||
default=False,
|
default=False,
|
||||||
description="Force (re)generating the description even if GenAI is disabled for this camera.",
|
description="Force (re)generating the description even if GenAI is disabled for this camera.",
|
||||||
)
|
)
|
||||||
|
|||||||
@ -1,5 +1,3 @@
|
|||||||
from typing import Union
|
|
||||||
|
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
from pydantic.json_schema import SkipJsonSchema
|
from pydantic.json_schema import SkipJsonSchema
|
||||||
|
|
||||||
@ -10,11 +8,11 @@ class ReviewQueryParams(BaseModel):
|
|||||||
cameras: str = "all"
|
cameras: str = "all"
|
||||||
labels: str = "all"
|
labels: str = "all"
|
||||||
zones: str = "all"
|
zones: str = "all"
|
||||||
reviewed: Union[int, SkipJsonSchema[None]] = None
|
reviewed: int | SkipJsonSchema[None] = None
|
||||||
limit: Union[int, SkipJsonSchema[None]] = None
|
limit: int | SkipJsonSchema[None] = None
|
||||||
severity: Union[SeverityEnum, SkipJsonSchema[None]] = None
|
severity: SeverityEnum | SkipJsonSchema[None] = None
|
||||||
before: Union[float, SkipJsonSchema[None]] = None
|
before: float | SkipJsonSchema[None] = None
|
||||||
after: Union[float, SkipJsonSchema[None]] = None
|
after: float | SkipJsonSchema[None] = None
|
||||||
|
|
||||||
|
|
||||||
class ReviewSummaryQueryParams(BaseModel):
|
class ReviewSummaryQueryParams(BaseModel):
|
||||||
@ -26,6 +24,6 @@ class ReviewSummaryQueryParams(BaseModel):
|
|||||||
|
|
||||||
class ReviewActivityMotionQueryParams(BaseModel):
|
class ReviewActivityMotionQueryParams(BaseModel):
|
||||||
cameras: str = "all"
|
cameras: str = "all"
|
||||||
before: Union[float, SkipJsonSchema[None]] = None
|
before: float | SkipJsonSchema[None] = None
|
||||||
after: Union[float, SkipJsonSchema[None]] = None
|
after: float | SkipJsonSchema[None] = None
|
||||||
scale: int = 30
|
scale: int = 30
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
from typing import Any, Dict, List, Optional
|
from typing import Any
|
||||||
|
|
||||||
from pydantic import BaseModel, Field
|
from pydantic import BaseModel, Field
|
||||||
|
|
||||||
@ -8,26 +8,26 @@ from frigate.config import GenAIProviderEnum
|
|||||||
class AppConfigSetBody(BaseModel):
|
class AppConfigSetBody(BaseModel):
|
||||||
requires_restart: int = 1
|
requires_restart: int = 1
|
||||||
update_topic: str | None = None
|
update_topic: str | None = None
|
||||||
config_data: Optional[Dict[str, Any]] = None
|
config_data: dict[str, Any] | None = None
|
||||||
skip_save: bool = False
|
skip_save: bool = False
|
||||||
|
|
||||||
|
|
||||||
class GenAIProbeBody(BaseModel):
|
class GenAIProbeBody(BaseModel):
|
||||||
provider: GenAIProviderEnum
|
provider: GenAIProviderEnum
|
||||||
api_key: Optional[str] = None
|
api_key: str | None = None
|
||||||
base_url: Optional[str] = None
|
base_url: str | None = None
|
||||||
provider_options: Dict[str, Any] = Field(default_factory=dict)
|
provider_options: dict[str, Any] = Field(default_factory=dict)
|
||||||
|
|
||||||
|
|
||||||
class AppPutPasswordBody(BaseModel):
|
class AppPutPasswordBody(BaseModel):
|
||||||
password: str
|
password: str
|
||||||
old_password: Optional[str] = None
|
old_password: str | None = None
|
||||||
|
|
||||||
|
|
||||||
class AppPostUsersBody(BaseModel):
|
class AppPostUsersBody(BaseModel):
|
||||||
username: str
|
username: str
|
||||||
password: str
|
password: str
|
||||||
role: Optional[str] = "viewer"
|
role: str | None = "viewer"
|
||||||
|
|
||||||
|
|
||||||
class AppPostLoginBody(BaseModel):
|
class AppPostLoginBody(BaseModel):
|
||||||
@ -47,7 +47,7 @@ class MediaSyncBody(BaseModel):
|
|||||||
dry_run: bool = Field(
|
dry_run: bool = Field(
|
||||||
default=True, description="If True, only report orphans without deleting them"
|
default=True, description="If True, only report orphans without deleting them"
|
||||||
)
|
)
|
||||||
media_types: List[str] = Field(
|
media_types: list[str] = Field(
|
||||||
default=["all"],
|
default=["all"],
|
||||||
description="Types of media to sync: 'all', 'event_snapshots', 'event_thumbnails', 'review_thumbnails', 'previews', 'exports', 'recordings'",
|
description="Types of media to sync: 'all', 'event_snapshots', 'event_thumbnails', 'review_thumbnails', 'previews', 'exports', 'recordings'",
|
||||||
)
|
)
|
||||||
|
|||||||
@ -1,5 +1,3 @@
|
|||||||
from typing import List, Optional
|
|
||||||
|
|
||||||
from pydantic import BaseModel, Field, model_validator
|
from pydantic import BaseModel, Field, model_validator
|
||||||
|
|
||||||
MAX_BATCH_EXPORT_ITEMS = 50
|
MAX_BATCH_EXPORT_ITEMS = 50
|
||||||
@ -9,18 +7,18 @@ class BatchExportItem(BaseModel):
|
|||||||
camera: str = Field(title="Camera name")
|
camera: str = Field(title="Camera name")
|
||||||
start_time: float = Field(title="Start time")
|
start_time: float = Field(title="Start time")
|
||||||
end_time: float = Field(title="End time")
|
end_time: float = Field(title="End time")
|
||||||
image_path: Optional[str] = Field(
|
image_path: str | None = Field(
|
||||||
default=None,
|
default=None,
|
||||||
title="Existing thumbnail path",
|
title="Existing thumbnail path",
|
||||||
description="Optional existing image to use as the export thumbnail",
|
description="Optional existing image to use as the export thumbnail",
|
||||||
)
|
)
|
||||||
friendly_name: Optional[str] = Field(
|
friendly_name: str | None = Field(
|
||||||
default=None,
|
default=None,
|
||||||
title="Friendly name",
|
title="Friendly name",
|
||||||
max_length=256,
|
max_length=256,
|
||||||
description="Optional friendly name for this specific export item",
|
description="Optional friendly name for this specific export item",
|
||||||
)
|
)
|
||||||
client_item_id: Optional[str] = Field(
|
client_item_id: str | None = Field(
|
||||||
default=None,
|
default=None,
|
||||||
title="Client item ID",
|
title="Client item ID",
|
||||||
max_length=128,
|
max_length=128,
|
||||||
@ -29,13 +27,13 @@ class BatchExportItem(BaseModel):
|
|||||||
|
|
||||||
|
|
||||||
class BatchExportBody(BaseModel):
|
class BatchExportBody(BaseModel):
|
||||||
items: List[BatchExportItem] = Field(
|
items: list[BatchExportItem] = Field(
|
||||||
title="Items",
|
title="Items",
|
||||||
min_length=1,
|
min_length=1,
|
||||||
max_length=MAX_BATCH_EXPORT_ITEMS,
|
max_length=MAX_BATCH_EXPORT_ITEMS,
|
||||||
description="List of export items. Each item has its own camera and time range.",
|
description="List of export items. Each item has its own camera and time range.",
|
||||||
)
|
)
|
||||||
export_case_id: Optional[str] = Field(
|
export_case_id: str | None = Field(
|
||||||
default=None,
|
default=None,
|
||||||
title="Export case ID",
|
title="Export case ID",
|
||||||
max_length=30,
|
max_length=30,
|
||||||
@ -44,13 +42,13 @@ class BatchExportBody(BaseModel):
|
|||||||
"existing case is temporarily admin-only until case-level ACLs exist."
|
"existing case is temporarily admin-only until case-level ACLs exist."
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
new_case_name: Optional[str] = Field(
|
new_case_name: str | None = Field(
|
||||||
default=None,
|
default=None,
|
||||||
title="New case name",
|
title="New case name",
|
||||||
max_length=100,
|
max_length=100,
|
||||||
description="Name of a new export case to create when export_case_id is omitted",
|
description="Name of a new export case to create when export_case_id is omitted",
|
||||||
)
|
)
|
||||||
new_case_description: Optional[str] = Field(
|
new_case_description: str | None = Field(
|
||||||
default=None,
|
default=None,
|
||||||
title="New case description",
|
title="New case description",
|
||||||
description="Optional description for a newly created export case",
|
description="Optional description for a newly created export case",
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
"""Chat API request models."""
|
"""Chat API request models."""
|
||||||
|
|
||||||
from typing import Any, Optional
|
from typing import Any
|
||||||
|
|
||||||
from pydantic import BaseModel, Field
|
from pydantic import BaseModel, Field
|
||||||
|
|
||||||
@ -11,7 +11,7 @@ class ChatMessage(BaseModel):
|
|||||||
role: str = Field(
|
role: str = Field(
|
||||||
description="Message role: 'user', 'assistant', 'system', or 'tool'"
|
description="Message role: 'user', 'assistant', 'system', or 'tool'"
|
||||||
)
|
)
|
||||||
content: Optional[Any] = Field(
|
content: Any | None = Field(
|
||||||
default=None,
|
default=None,
|
||||||
description=(
|
description=(
|
||||||
"Message content. Usually a string, but may be a multimodal content "
|
"Message content. Usually a string, but may be a multimodal content "
|
||||||
@ -19,13 +19,13 @@ class ChatMessage(BaseModel):
|
|||||||
"request tool calls."
|
"request tool calls."
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
tool_call_id: Optional[str] = Field(
|
tool_call_id: str | None = Field(
|
||||||
default=None, description="For tool messages, the ID of the tool call"
|
default=None, description="For tool messages, the ID of the tool call"
|
||||||
)
|
)
|
||||||
name: Optional[str] = Field(
|
name: str | None = Field(
|
||||||
default=None, description="For tool messages, the tool name"
|
default=None, description="For tool messages, the tool name"
|
||||||
)
|
)
|
||||||
tool_calls: Optional[list[dict[str, Any]]] = Field(
|
tool_calls: list[dict[str, Any]] | None = Field(
|
||||||
default=None,
|
default=None,
|
||||||
description=(
|
description=(
|
||||||
"For assistant messages replayed from prior turns, the OpenAI-format "
|
"For assistant messages replayed from prior turns, the OpenAI-format "
|
||||||
@ -52,7 +52,7 @@ class ChatCompletionRequest(BaseModel):
|
|||||||
default=False,
|
default=False,
|
||||||
description="If true, stream the final assistant response in the body as newline-delimited JSON.",
|
description="If true, stream the final assistant response in the body as newline-delimited JSON.",
|
||||||
)
|
)
|
||||||
enable_thinking: Optional[bool] = Field(
|
enable_thinking: bool | None = Field(
|
||||||
default=None,
|
default=None,
|
||||||
description=(
|
description=(
|
||||||
"Per-request thinking toggle. None means use the provider default. "
|
"Per-request thinking toggle. None means use the provider default. "
|
||||||
|
|||||||
@ -1,5 +1,3 @@
|
|||||||
from typing import Dict, List, Tuple
|
|
||||||
|
|
||||||
from pydantic import BaseModel, Field
|
from pydantic import BaseModel, Field
|
||||||
|
|
||||||
|
|
||||||
@ -12,14 +10,14 @@ class AudioTranscriptionBody(BaseModel):
|
|||||||
|
|
||||||
|
|
||||||
class DeleteFaceImagesBody(BaseModel):
|
class DeleteFaceImagesBody(BaseModel):
|
||||||
ids: List[str] = Field(
|
ids: list[str] = Field(
|
||||||
description="List of image filenames to delete from the face folder"
|
description="List of image filenames to delete from the face folder"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class GenerateStateExamplesBody(BaseModel):
|
class GenerateStateExamplesBody(BaseModel):
|
||||||
model_name: str = Field(description="Name of the classification model")
|
model_name: str = Field(description="Name of the classification model")
|
||||||
cameras: Dict[str, Tuple[float, float, float, float]] = Field(
|
cameras: dict[str, tuple[float, float, float, float]] = Field(
|
||||||
description="Dictionary mapping camera names to normalized crop coordinates in [x1, y1, x2, y2] format (values 0-1)"
|
description="Dictionary mapping camera names to normalized crop coordinates in [x1, y1, x2, y2] format (values 0-1)"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@ -1,5 +1,3 @@
|
|||||||
from typing import List, Optional, Union
|
|
||||||
|
|
||||||
from pydantic import BaseModel, Field
|
from pydantic import BaseModel, Field
|
||||||
|
|
||||||
from frigate.config.classification import TriggerType
|
from frigate.config.classification import TriggerType
|
||||||
@ -7,49 +5,47 @@ from frigate.config.classification import TriggerType
|
|||||||
|
|
||||||
class EventsSubLabelBody(BaseModel):
|
class EventsSubLabelBody(BaseModel):
|
||||||
subLabel: str = Field(title="Sub label", max_length=100)
|
subLabel: str = Field(title="Sub label", max_length=100)
|
||||||
subLabelScore: Optional[float] = Field(
|
subLabelScore: float | None = Field(
|
||||||
title="Score for sub label", default=None, gt=0.0, le=1.0
|
title="Score for sub label", default=None, gt=0.0, le=1.0
|
||||||
)
|
)
|
||||||
camera: Optional[str] = Field(
|
camera: str | None = Field(title="Camera this object is detected on.", default=None)
|
||||||
title="Camera this object is detected on.", default=None
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class EventsLPRBody(BaseModel):
|
class EventsLPRBody(BaseModel):
|
||||||
recognizedLicensePlate: str = Field(
|
recognizedLicensePlate: str = Field(
|
||||||
title="Recognized License Plate", max_length=100
|
title="Recognized License Plate", max_length=100
|
||||||
)
|
)
|
||||||
recognizedLicensePlateScore: Optional[float] = Field(
|
recognizedLicensePlateScore: float | None = Field(
|
||||||
title="Score for recognized license plate", default=None, gt=0.0, le=1.0
|
title="Score for recognized license plate", default=None, gt=0.0, le=1.0
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class EventsAttributesBody(BaseModel):
|
class EventsAttributesBody(BaseModel):
|
||||||
attributes: List[str] = Field(
|
attributes: list[str] = Field(
|
||||||
title="Selected classification attributes for the event",
|
title="Selected classification attributes for the event",
|
||||||
default_factory=list,
|
default_factory=list,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class EventsDescriptionBody(BaseModel):
|
class EventsDescriptionBody(BaseModel):
|
||||||
description: Union[str, None] = Field(title="The description of the event")
|
description: str | None = Field(title="The description of the event")
|
||||||
|
|
||||||
|
|
||||||
class EventsCreateBody(BaseModel):
|
class EventsCreateBody(BaseModel):
|
||||||
sub_label: Optional[str] = None
|
sub_label: str | None = None
|
||||||
score: Optional[float] = 0
|
score: float | None = 0
|
||||||
duration: Optional[int] = 30
|
duration: int | None = 30
|
||||||
include_recording: Optional[bool] = True
|
include_recording: bool | None = True
|
||||||
draw: Optional[dict] = {}
|
draw: dict | None = {}
|
||||||
pre_capture: Optional[int] = None
|
pre_capture: int | None = None
|
||||||
|
|
||||||
|
|
||||||
class EventsEndBody(BaseModel):
|
class EventsEndBody(BaseModel):
|
||||||
end_time: Optional[float] = None
|
end_time: float | None = None
|
||||||
|
|
||||||
|
|
||||||
class EventsDeleteBody(BaseModel):
|
class EventsDeleteBody(BaseModel):
|
||||||
event_ids: List[str] = Field(title="The event IDs to delete")
|
event_ids: list[str] = Field(title="The event IDs to delete")
|
||||||
|
|
||||||
|
|
||||||
class SubmitPlusBody(BaseModel):
|
class SubmitPlusBody(BaseModel):
|
||||||
|
|||||||
@ -1,7 +1,5 @@
|
|||||||
"""Request bodies for bulk export operations."""
|
"""Request bodies for bulk export operations."""
|
||||||
|
|
||||||
from typing import Optional
|
|
||||||
|
|
||||||
from pydantic import BaseModel, Field, conlist, constr
|
from pydantic import BaseModel, Field, conlist, constr
|
||||||
|
|
||||||
|
|
||||||
@ -17,7 +15,7 @@ class ExportBulkReassignBody(BaseModel):
|
|||||||
|
|
||||||
# List of export IDs with at least one element and each element with at least one char
|
# List of export IDs with at least one element and each element with at least one char
|
||||||
ids: conlist(constr(min_length=1), min_length=1)
|
ids: conlist(constr(min_length=1), min_length=1)
|
||||||
export_case_id: Optional[str] = Field(
|
export_case_id: str | None = Field(
|
||||||
default=None,
|
default=None,
|
||||||
max_length=30,
|
max_length=30,
|
||||||
description="Case ID to assign to, or null to unassign from current case",
|
description="Case ID to assign to, or null to unassign from current case",
|
||||||
|
|||||||
@ -1,5 +1,3 @@
|
|||||||
from typing import Optional
|
|
||||||
|
|
||||||
from pydantic import BaseModel, Field
|
from pydantic import BaseModel, Field
|
||||||
|
|
||||||
|
|
||||||
@ -7,7 +5,7 @@ class ExportCaseCreateBody(BaseModel):
|
|||||||
"""Request body for creating a new export case."""
|
"""Request body for creating a new export case."""
|
||||||
|
|
||||||
name: str = Field(max_length=100, description="Friendly name of the export case")
|
name: str = Field(max_length=100, description="Friendly name of the export case")
|
||||||
description: Optional[str] = Field(
|
description: str | None = Field(
|
||||||
default=None, description="Optional description of the export case"
|
default=None, description="Optional description of the export case"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -15,11 +13,11 @@ class ExportCaseCreateBody(BaseModel):
|
|||||||
class ExportCaseUpdateBody(BaseModel):
|
class ExportCaseUpdateBody(BaseModel):
|
||||||
"""Request body for updating an existing export case."""
|
"""Request body for updating an existing export case."""
|
||||||
|
|
||||||
name: Optional[str] = Field(
|
name: str | None = Field(
|
||||||
default=None,
|
default=None,
|
||||||
max_length=100,
|
max_length=100,
|
||||||
description="Updated friendly name of the export case",
|
description="Updated friendly name of the export case",
|
||||||
)
|
)
|
||||||
description: Optional[str] = Field(
|
description: str | None = Field(
|
||||||
default=None, description="Updated description of the export case"
|
default=None, description="Updated description of the export case"
|
||||||
)
|
)
|
||||||
|
|||||||
@ -1,5 +1,3 @@
|
|||||||
from typing import Optional, Union
|
|
||||||
|
|
||||||
from pydantic import BaseModel, Field
|
from pydantic import BaseModel, Field
|
||||||
from pydantic.json_schema import SkipJsonSchema
|
from pydantic.json_schema import SkipJsonSchema
|
||||||
|
|
||||||
@ -13,15 +11,15 @@ class ExportRecordingsBody(BaseModel):
|
|||||||
source: PlaybackSourceEnum = Field(
|
source: PlaybackSourceEnum = Field(
|
||||||
default=PlaybackSourceEnum.recordings, title="Playback source"
|
default=PlaybackSourceEnum.recordings, title="Playback source"
|
||||||
)
|
)
|
||||||
name: Optional[str] = Field(title="Friendly name", default=None, max_length=256)
|
name: str | None = Field(title="Friendly name", default=None, max_length=256)
|
||||||
image_path: Union[str, SkipJsonSchema[None]] = None
|
image_path: str | SkipJsonSchema[None] = None
|
||||||
export_case_id: Optional[str] = Field(
|
export_case_id: str | None = Field(
|
||||||
default=None,
|
default=None,
|
||||||
title="Export case ID",
|
title="Export case ID",
|
||||||
max_length=30,
|
max_length=30,
|
||||||
description="ID of the export case to assign this export to",
|
description="ID of the export case to assign this export to",
|
||||||
)
|
)
|
||||||
chapters: Optional[ChaptersEnum] = Field(
|
chapters: ChaptersEnum | None = Field(
|
||||||
default=None,
|
default=None,
|
||||||
title="Chapter mode",
|
title="Chapter mode",
|
||||||
description=(
|
description=(
|
||||||
@ -36,19 +34,19 @@ class ExportRecordingsCustomBody(BaseModel):
|
|||||||
default=PlaybackSourceEnum.recordings, title="Playback source"
|
default=PlaybackSourceEnum.recordings, title="Playback source"
|
||||||
)
|
)
|
||||||
name: str = Field(title="Friendly name", default=None, max_length=256)
|
name: str = Field(title="Friendly name", default=None, max_length=256)
|
||||||
image_path: Union[str, SkipJsonSchema[None]] = None
|
image_path: str | SkipJsonSchema[None] = None
|
||||||
export_case_id: Optional[str] = Field(
|
export_case_id: str | None = Field(
|
||||||
default=None,
|
default=None,
|
||||||
title="Export case ID",
|
title="Export case ID",
|
||||||
max_length=30,
|
max_length=30,
|
||||||
description="ID of the export case to assign this export to",
|
description="ID of the export case to assign this export to",
|
||||||
)
|
)
|
||||||
ffmpeg_input_args: Optional[str] = Field(
|
ffmpeg_input_args: str | None = Field(
|
||||||
default=None,
|
default=None,
|
||||||
title="FFmpeg input arguments",
|
title="FFmpeg input arguments",
|
||||||
description="Custom FFmpeg input arguments. If not provided, defaults to timelapse input args.",
|
description="Custom FFmpeg input arguments. If not provided, defaults to timelapse input args.",
|
||||||
)
|
)
|
||||||
ffmpeg_output_args: Optional[str] = Field(
|
ffmpeg_output_args: str | None = Field(
|
||||||
default=None,
|
default=None,
|
||||||
title="FFmpeg output arguments",
|
title="FFmpeg output arguments",
|
||||||
description="Custom FFmpeg output arguments. If not provided, defaults to timelapse output args.",
|
description="Custom FFmpeg output arguments. If not provided, defaults to timelapse output args.",
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
"""Chat API response models."""
|
"""Chat API response models."""
|
||||||
|
|
||||||
from typing import Any, Optional
|
from typing import Any
|
||||||
|
|
||||||
from pydantic import BaseModel, Field
|
from pydantic import BaseModel, Field
|
||||||
|
|
||||||
@ -17,14 +17,14 @@ class ChatMessageResponse(BaseModel):
|
|||||||
"""A message in the chat response."""
|
"""A message in the chat response."""
|
||||||
|
|
||||||
role: str = Field(description="Message role")
|
role: str = Field(description="Message role")
|
||||||
content: Optional[str] = Field(
|
content: str | None = Field(
|
||||||
default=None, description="Message content (None if tool calls present)"
|
default=None, description="Message content (None if tool calls present)"
|
||||||
)
|
)
|
||||||
reasoning: Optional[str] = Field(
|
reasoning: str | None = Field(
|
||||||
default=None,
|
default=None,
|
||||||
description="Separated reasoning/thinking trace if the model emitted one",
|
description="Separated reasoning/thinking trace if the model emitted one",
|
||||||
)
|
)
|
||||||
tool_calls: Optional[list[ToolCallInvocation]] = Field(
|
tool_calls: list[ToolCallInvocation] | None = Field(
|
||||||
default=None, description="Tool calls if LLM wants to call tools"
|
default=None, description="Tool calls if LLM wants to call tools"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@ -1,9 +1,7 @@
|
|||||||
from typing import Dict, List, Optional
|
|
||||||
|
|
||||||
from pydantic import BaseModel, Field, RootModel
|
from pydantic import BaseModel, Field, RootModel
|
||||||
|
|
||||||
|
|
||||||
class FacesResponse(RootModel[Dict[str, List[str]]]):
|
class FacesResponse(RootModel[dict[str, list[str]]]):
|
||||||
"""Response model for the get_faces endpoint.
|
"""Response model for the get_faces endpoint.
|
||||||
|
|
||||||
Returns a mapping of face names to lists of image filenames.
|
Returns a mapping of face names to lists of image filenames.
|
||||||
@ -17,7 +15,7 @@ class FacesResponse(RootModel[Dict[str, List[str]]]):
|
|||||||
}
|
}
|
||||||
"""
|
"""
|
||||||
|
|
||||||
root: Dict[str, List[str]] = Field(
|
root: dict[str, list[str]] = Field(
|
||||||
default_factory=dict,
|
default_factory=dict,
|
||||||
description="Dictionary mapping face names to lists of image filenames",
|
description="Dictionary mapping face names to lists of image filenames",
|
||||||
)
|
)
|
||||||
@ -30,9 +28,9 @@ class FaceRecognitionResponse(BaseModel):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
success: bool = Field(description="Whether the face recognition was successful")
|
success: bool = Field(description="Whether the face recognition was successful")
|
||||||
score: Optional[float] = Field(
|
score: float | None = Field(
|
||||||
default=None, description="Confidence score of the recognition (0-1)"
|
default=None, description="Confidence score of the recognition (0-1)"
|
||||||
)
|
)
|
||||||
face_name: Optional[str] = Field(
|
face_name: str | None = Field(
|
||||||
default=None, description="The recognized face name if successful"
|
default=None, description="The recognized face name if successful"
|
||||||
)
|
)
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
from typing import Any, Optional
|
from typing import Any
|
||||||
|
|
||||||
from pydantic import BaseModel, ConfigDict
|
from pydantic import BaseModel, ConfigDict
|
||||||
|
|
||||||
@ -6,20 +6,20 @@ from pydantic import BaseModel, ConfigDict
|
|||||||
class EventResponse(BaseModel):
|
class EventResponse(BaseModel):
|
||||||
id: str
|
id: str
|
||||||
label: str
|
label: str
|
||||||
sub_label: Optional[str]
|
sub_label: str | None
|
||||||
camera: str
|
camera: str
|
||||||
start_time: float
|
start_time: float
|
||||||
end_time: Optional[float]
|
end_time: float | None
|
||||||
false_positive: Optional[bool]
|
false_positive: bool | None
|
||||||
zones: list[str]
|
zones: list[str]
|
||||||
thumbnail: Optional[str]
|
thumbnail: str | None
|
||||||
has_clip: bool
|
has_clip: bool
|
||||||
has_snapshot: bool
|
has_snapshot: bool
|
||||||
retain_indefinitely: bool
|
retain_indefinitely: bool
|
||||||
plus_id: Optional[str]
|
plus_id: str | None
|
||||||
model_hash: Optional[str]
|
model_hash: str | None
|
||||||
detector_type: Optional[str]
|
detector_type: str | None
|
||||||
model_type: Optional[str]
|
model_type: str | None
|
||||||
data: dict[str, Any]
|
data: dict[str, Any]
|
||||||
|
|
||||||
model_config = ConfigDict(protected_namespaces=())
|
model_config = ConfigDict(protected_namespaces=())
|
||||||
|
|||||||
@ -1,5 +1,3 @@
|
|||||||
from typing import List, Optional
|
|
||||||
|
|
||||||
from pydantic import BaseModel, Field
|
from pydantic import BaseModel, Field
|
||||||
|
|
||||||
|
|
||||||
@ -8,7 +6,7 @@ class ExportCaseModel(BaseModel):
|
|||||||
|
|
||||||
id: str = Field(description="Unique identifier for the export case")
|
id: str = Field(description="Unique identifier for the export case")
|
||||||
name: str = Field(description="Friendly name of the export case")
|
name: str = Field(description="Friendly name of the export case")
|
||||||
description: Optional[str] = Field(
|
description: str | None = Field(
|
||||||
default=None, description="Optional description of the export case"
|
default=None, description="Optional description of the export case"
|
||||||
)
|
)
|
||||||
created_at: float = Field(
|
created_at: float = Field(
|
||||||
@ -19,4 +17,4 @@ class ExportCaseModel(BaseModel):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
ExportCasesResponse = List[ExportCaseModel]
|
ExportCasesResponse = list[ExportCaseModel]
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
from typing import Any, List, Optional
|
from typing import Any
|
||||||
|
|
||||||
from pydantic import BaseModel, Field
|
from pydantic import BaseModel, Field
|
||||||
|
|
||||||
@ -15,7 +15,7 @@ class ExportModel(BaseModel):
|
|||||||
in_progress: bool = Field(
|
in_progress: bool = Field(
|
||||||
description="Whether the export is currently being processed"
|
description="Whether the export is currently being processed"
|
||||||
)
|
)
|
||||||
export_case_id: Optional[str] = Field(
|
export_case_id: str | None = Field(
|
||||||
default=None, description="ID of the export case this export belongs to"
|
default=None, description="ID of the export case this export belongs to"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -25,10 +25,10 @@ class StartExportResponse(BaseModel):
|
|||||||
|
|
||||||
success: bool = Field(description="Whether the export was started successfully")
|
success: bool = Field(description="Whether the export was started successfully")
|
||||||
message: str = Field(description="Status or error message")
|
message: str = Field(description="Status or error message")
|
||||||
export_id: Optional[str] = Field(
|
export_id: str | None = Field(
|
||||||
default=None, description="The export ID if successfully started"
|
default=None, description="The export ID if successfully started"
|
||||||
)
|
)
|
||||||
status: Optional[str] = Field(
|
status: str | None = Field(
|
||||||
default=None,
|
default=None,
|
||||||
description="Queue status for the export job",
|
description="Queue status for the export job",
|
||||||
)
|
)
|
||||||
@ -38,24 +38,24 @@ class BatchExportResultModel(BaseModel):
|
|||||||
"""Per-item result for a batch export request."""
|
"""Per-item result for a batch export request."""
|
||||||
|
|
||||||
camera: str = Field(description="Camera name for this export attempt")
|
camera: str = Field(description="Camera name for this export attempt")
|
||||||
export_id: Optional[str] = Field(
|
export_id: str | None = Field(
|
||||||
default=None,
|
default=None,
|
||||||
description="The export ID when the export was successfully queued",
|
description="The export ID when the export was successfully queued",
|
||||||
)
|
)
|
||||||
success: bool = Field(description="Whether the export was successfully queued")
|
success: bool = Field(description="Whether the export was successfully queued")
|
||||||
status: Optional[str] = Field(
|
status: str | None = Field(
|
||||||
default=None,
|
default=None,
|
||||||
description="Queue status for this camera export",
|
description="Queue status for this camera export",
|
||||||
)
|
)
|
||||||
error: Optional[str] = Field(
|
error: str | None = Field(
|
||||||
default=None,
|
default=None,
|
||||||
description="Validation or queueing error for this item, if any",
|
description="Validation or queueing error for this item, if any",
|
||||||
)
|
)
|
||||||
item_index: Optional[int] = Field(
|
item_index: int | None = Field(
|
||||||
default=None,
|
default=None,
|
||||||
description="Zero-based index of this result within the request items list",
|
description="Zero-based index of this result within the request items list",
|
||||||
)
|
)
|
||||||
client_item_id: Optional[str] = Field(
|
client_item_id: str | None = Field(
|
||||||
default=None,
|
default=None,
|
||||||
description="Opaque client-supplied item identifier echoed from the request",
|
description="Opaque client-supplied item identifier echoed from the request",
|
||||||
)
|
)
|
||||||
@ -64,12 +64,12 @@ class BatchExportResultModel(BaseModel):
|
|||||||
class BatchExportResponse(BaseModel):
|
class BatchExportResponse(BaseModel):
|
||||||
"""Response model for starting an export batch."""
|
"""Response model for starting an export batch."""
|
||||||
|
|
||||||
export_case_id: Optional[str] = Field(
|
export_case_id: str | None = Field(
|
||||||
default=None,
|
default=None,
|
||||||
description="Export case ID associated with the batch",
|
description="Export case ID associated with the batch",
|
||||||
)
|
)
|
||||||
export_ids: List[str] = Field(description="Export IDs successfully queued")
|
export_ids: list[str] = Field(description="Export IDs successfully queued")
|
||||||
results: List[BatchExportResultModel] = Field(
|
results: list[BatchExportResultModel] = Field(
|
||||||
description="Per-item batch export results"
|
description="Per-item batch export results"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -81,29 +81,29 @@ class ExportJobModel(BaseModel):
|
|||||||
job_type: str = Field(description="Job type")
|
job_type: str = Field(description="Job type")
|
||||||
status: str = Field(description="Current job status")
|
status: str = Field(description="Current job status")
|
||||||
camera: str = Field(description="Camera associated with this export job")
|
camera: str = Field(description="Camera associated with this export job")
|
||||||
name: Optional[str] = Field(
|
name: str | None = Field(
|
||||||
default=None,
|
default=None,
|
||||||
description="Friendly name for the export",
|
description="Friendly name for the export",
|
||||||
)
|
)
|
||||||
export_case_id: Optional[str] = Field(
|
export_case_id: str | None = Field(
|
||||||
default=None,
|
default=None,
|
||||||
description="ID of the export case this export belongs to",
|
description="ID of the export case this export belongs to",
|
||||||
)
|
)
|
||||||
request_start_time: float = Field(description="Requested export start time")
|
request_start_time: float = Field(description="Requested export start time")
|
||||||
request_end_time: float = Field(description="Requested export end time")
|
request_end_time: float = Field(description="Requested export end time")
|
||||||
start_time: Optional[float] = Field(
|
start_time: float | None = Field(
|
||||||
default=None,
|
default=None,
|
||||||
description="Unix timestamp when execution started",
|
description="Unix timestamp when execution started",
|
||||||
)
|
)
|
||||||
end_time: Optional[float] = Field(
|
end_time: float | None = Field(
|
||||||
default=None,
|
default=None,
|
||||||
description="Unix timestamp when execution completed",
|
description="Unix timestamp when execution completed",
|
||||||
)
|
)
|
||||||
error_message: Optional[str] = Field(
|
error_message: str | None = Field(
|
||||||
default=None,
|
default=None,
|
||||||
description="Error message for failed jobs",
|
description="Error message for failed jobs",
|
||||||
)
|
)
|
||||||
results: Optional[dict[str, Any]] = Field(
|
results: dict[str, Any] | None = Field(
|
||||||
default=None,
|
default=None,
|
||||||
description="Result metadata for completed jobs",
|
description="Result metadata for completed jobs",
|
||||||
)
|
)
|
||||||
@ -117,7 +117,7 @@ class ExportJobModel(BaseModel):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
ExportJobsResponse = List[ExportJobModel]
|
ExportJobsResponse = list[ExportJobModel]
|
||||||
|
|
||||||
|
|
||||||
ExportsResponse = List[ExportModel]
|
ExportsResponse = list[ExportModel]
|
||||||
|
|||||||
@ -1,5 +1,3 @@
|
|||||||
from typing import List
|
|
||||||
|
|
||||||
from pydantic import BaseModel, Field
|
from pydantic import BaseModel, Field
|
||||||
|
|
||||||
|
|
||||||
@ -13,5 +11,5 @@ class PreviewModel(BaseModel):
|
|||||||
end: float = Field(description="Unix timestamp when the preview ends")
|
end: float = Field(description="Unix timestamp when the preview ends")
|
||||||
|
|
||||||
|
|
||||||
PreviewsResponse = List[PreviewModel]
|
PreviewsResponse = list[PreviewModel]
|
||||||
PreviewFramesResponse = List[str]
|
PreviewFramesResponse = list[str]
|
||||||
|
|||||||
@ -1,5 +1,4 @@
|
|||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from typing import Dict
|
|
||||||
|
|
||||||
from pydantic import BaseModel, Json
|
from pydantic import BaseModel, Json
|
||||||
|
|
||||||
@ -34,7 +33,7 @@ class DayReview(BaseModel):
|
|||||||
|
|
||||||
class ReviewSummaryResponse(BaseModel):
|
class ReviewSummaryResponse(BaseModel):
|
||||||
last24Hours: Last24HoursReview
|
last24Hours: Last24HoursReview
|
||||||
root: Dict[str, DayReview]
|
root: dict[str, DayReview]
|
||||||
|
|
||||||
|
|
||||||
class ReviewActivityMotionResponse(BaseModel):
|
class ReviewActivityMotionResponse(BaseModel):
|
||||||
|
|||||||
@ -10,7 +10,6 @@ import random
|
|||||||
import string
|
import string
|
||||||
from functools import reduce
|
from functools import reduce
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import List
|
|
||||||
from urllib.parse import unquote
|
from urllib.parse import unquote
|
||||||
|
|
||||||
import numpy as np
|
import numpy as np
|
||||||
@ -97,7 +96,7 @@ def _build_attribute_filter_clause(attributes: str):
|
|||||||
)
|
)
|
||||||
def events(
|
def events(
|
||||||
params: EventsQueryParams = Depends(),
|
params: EventsQueryParams = Depends(),
|
||||||
allowed_cameras: List[str] = Depends(get_allowed_cameras_for_filter),
|
allowed_cameras: list[str] = Depends(get_allowed_cameras_for_filter),
|
||||||
):
|
):
|
||||||
camera = params.camera
|
camera = params.camera
|
||||||
cameras = params.cameras
|
cameras = params.cameras
|
||||||
@ -171,7 +170,7 @@ def events(
|
|||||||
]
|
]
|
||||||
|
|
||||||
if camera != "all":
|
if camera != "all":
|
||||||
clauses.append((Event.camera == camera))
|
clauses.append(Event.camera == camera)
|
||||||
|
|
||||||
if cameras != "all":
|
if cameras != "all":
|
||||||
requested = set(cameras.split(","))
|
requested = set(cameras.split(","))
|
||||||
@ -181,11 +180,11 @@ def events(
|
|||||||
camera_list = list(filtered)
|
camera_list = list(filtered)
|
||||||
else:
|
else:
|
||||||
camera_list = allowed_cameras
|
camera_list = allowed_cameras
|
||||||
clauses.append((Event.camera << camera_list))
|
clauses.append(Event.camera << camera_list)
|
||||||
|
|
||||||
if labels != "all":
|
if labels != "all":
|
||||||
label_list = labels.split(",")
|
label_list = labels.split(",")
|
||||||
clauses.append((Event.label << label_list))
|
clauses.append(Event.label << label_list)
|
||||||
|
|
||||||
if sub_labels != "all":
|
if sub_labels != "all":
|
||||||
# use matching so joined sub labels are included
|
# use matching so joined sub labels are included
|
||||||
@ -196,24 +195,24 @@ def events(
|
|||||||
|
|
||||||
if "None" in filtered_sub_labels:
|
if "None" in filtered_sub_labels:
|
||||||
filtered_sub_labels.remove("None")
|
filtered_sub_labels.remove("None")
|
||||||
sub_label_clauses.append((Event.sub_label.is_null()))
|
sub_label_clauses.append(Event.sub_label.is_null())
|
||||||
|
|
||||||
for label in filtered_sub_labels:
|
for label in filtered_sub_labels:
|
||||||
lowered = label.lower()
|
lowered = label.lower()
|
||||||
sub_label_clauses.append(
|
sub_label_clauses.append(
|
||||||
(fn.LOWER(Event.sub_label.cast("text")) == lowered)
|
fn.LOWER(Event.sub_label.cast("text")) == lowered
|
||||||
) # include exact matches (case-insensitive)
|
) # include exact matches (case-insensitive)
|
||||||
|
|
||||||
# include this label when part of a list (LIKE is case-insensitive in sqlite for ASCII)
|
# include this label when part of a list (LIKE is case-insensitive in sqlite for ASCII)
|
||||||
sub_label_clauses.append(
|
sub_label_clauses.append(
|
||||||
(fn.LOWER(Event.sub_label.cast("text")) % f"*{lowered},*")
|
fn.LOWER(Event.sub_label.cast("text")) % f"*{lowered},*"
|
||||||
)
|
)
|
||||||
sub_label_clauses.append(
|
sub_label_clauses.append(
|
||||||
(fn.LOWER(Event.sub_label.cast("text")) % f"*, {lowered}*")
|
fn.LOWER(Event.sub_label.cast("text")) % f"*, {lowered}*"
|
||||||
)
|
)
|
||||||
|
|
||||||
sub_label_clause = reduce(operator.or_, sub_label_clauses)
|
sub_label_clause = reduce(operator.or_, sub_label_clauses)
|
||||||
clauses.append((sub_label_clause))
|
clauses.append(sub_label_clause)
|
||||||
|
|
||||||
if attributes != "all":
|
if attributes != "all":
|
||||||
# Custom classification results are stored as data[model_name] = result_value
|
# Custom classification results are stored as data[model_name] = result_value
|
||||||
@ -257,19 +256,19 @@ def events(
|
|||||||
|
|
||||||
if "None" in filtered_zones:
|
if "None" in filtered_zones:
|
||||||
filtered_zones.remove("None")
|
filtered_zones.remove("None")
|
||||||
zone_clauses.append((Event.zones.length() == 0))
|
zone_clauses.append(Event.zones.length() == 0)
|
||||||
|
|
||||||
for zone in filtered_zones:
|
for zone in filtered_zones:
|
||||||
zone_clauses.append((Event.zones.cast("text") % f'*"{zone}"*'))
|
zone_clauses.append(Event.zones.cast("text") % f'*"{zone}"*')
|
||||||
|
|
||||||
zone_clause = reduce(operator.or_, zone_clauses)
|
zone_clause = reduce(operator.or_, zone_clauses)
|
||||||
clauses.append((zone_clause))
|
clauses.append(zone_clause)
|
||||||
|
|
||||||
if after:
|
if after:
|
||||||
clauses.append((Event.start_time > after))
|
clauses.append(Event.start_time > after)
|
||||||
|
|
||||||
if before:
|
if before:
|
||||||
clauses.append((Event.start_time < before))
|
clauses.append(Event.start_time < before)
|
||||||
|
|
||||||
if time_range != DEFAULT_TIME_RANGE:
|
if time_range != DEFAULT_TIME_RANGE:
|
||||||
# get timezone arg to ensure browser times are used
|
# get timezone arg to ensure browser times are used
|
||||||
@ -289,62 +288,60 @@ def events(
|
|||||||
# should use or operator
|
# should use or operator
|
||||||
if time_after > time_before:
|
if time_after > time_before:
|
||||||
clauses.append(
|
clauses.append(
|
||||||
(
|
|
||||||
reduce(
|
reduce(
|
||||||
operator.or_,
|
operator.or_,
|
||||||
[(start_hour_fun > time_after), (start_hour_fun < time_before)],
|
[(start_hour_fun > time_after), (start_hour_fun < time_before)],
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
)
|
|
||||||
# all other cases should be and operator
|
# all other cases should be and operator
|
||||||
else:
|
else:
|
||||||
clauses.append((start_hour_fun > time_after))
|
clauses.append(start_hour_fun > time_after)
|
||||||
clauses.append((start_hour_fun < time_before))
|
clauses.append(start_hour_fun < time_before)
|
||||||
|
|
||||||
if has_clip is not None:
|
if has_clip is not None:
|
||||||
clauses.append((Event.has_clip == has_clip))
|
clauses.append(Event.has_clip == has_clip)
|
||||||
|
|
||||||
if has_snapshot is not None:
|
if has_snapshot is not None:
|
||||||
clauses.append((Event.has_snapshot == has_snapshot))
|
clauses.append(Event.has_snapshot == has_snapshot)
|
||||||
|
|
||||||
if in_progress is not None:
|
if in_progress is not None:
|
||||||
clauses.append((Event.end_time.is_null(in_progress)))
|
clauses.append(Event.end_time.is_null(in_progress))
|
||||||
|
|
||||||
if include_thumbnails:
|
if include_thumbnails:
|
||||||
selected_columns.append(Event.thumbnail)
|
selected_columns.append(Event.thumbnail)
|
||||||
|
|
||||||
if favorites:
|
if favorites:
|
||||||
clauses.append((Event.retain_indefinitely == favorites))
|
clauses.append(Event.retain_indefinitely == favorites)
|
||||||
|
|
||||||
if max_score is not None:
|
if max_score is not None:
|
||||||
clauses.append((Event.data["score"] <= max_score))
|
clauses.append(Event.data["score"] <= max_score)
|
||||||
|
|
||||||
if min_score is not None:
|
if min_score is not None:
|
||||||
clauses.append((Event.data["score"] >= min_score))
|
clauses.append(Event.data["score"] >= min_score)
|
||||||
|
|
||||||
if max_speed is not None:
|
if max_speed is not None:
|
||||||
clauses.append((Event.data["average_estimated_speed"] <= max_speed))
|
clauses.append(Event.data["average_estimated_speed"] <= max_speed)
|
||||||
|
|
||||||
if min_speed is not None:
|
if min_speed is not None:
|
||||||
clauses.append((Event.data["average_estimated_speed"] >= min_speed))
|
clauses.append(Event.data["average_estimated_speed"] >= min_speed)
|
||||||
|
|
||||||
if min_length is not None:
|
if min_length is not None:
|
||||||
clauses.append(((Event.end_time - Event.start_time) >= min_length))
|
clauses.append((Event.end_time - Event.start_time) >= min_length)
|
||||||
|
|
||||||
if max_length is not None:
|
if max_length is not None:
|
||||||
clauses.append(((Event.end_time - Event.start_time) <= max_length))
|
clauses.append((Event.end_time - Event.start_time) <= max_length)
|
||||||
|
|
||||||
if is_submitted is not None:
|
if is_submitted is not None:
|
||||||
if is_submitted == 0:
|
if is_submitted == 0:
|
||||||
clauses.append((Event.plus_id.is_null()))
|
clauses.append(Event.plus_id.is_null())
|
||||||
elif is_submitted > 0:
|
elif is_submitted > 0:
|
||||||
clauses.append((Event.plus_id != ""))
|
clauses.append(Event.plus_id != "")
|
||||||
|
|
||||||
if event_id is not None:
|
if event_id is not None:
|
||||||
clauses.append((Event.id == event_id))
|
clauses.append(Event.id == event_id)
|
||||||
|
|
||||||
if len(clauses) == 0:
|
if len(clauses) == 0:
|
||||||
clauses.append((True))
|
clauses.append(True)
|
||||||
|
|
||||||
if sort:
|
if sort:
|
||||||
if sort == "score_asc":
|
if sort == "score_asc":
|
||||||
@ -387,7 +384,7 @@ def events(
|
|||||||
)
|
)
|
||||||
def events_explore(
|
def events_explore(
|
||||||
limit: int = 10,
|
limit: int = 10,
|
||||||
allowed_cameras: List[str] = Depends(get_allowed_cameras_for_filter),
|
allowed_cameras: list[str] = Depends(get_allowed_cameras_for_filter),
|
||||||
):
|
):
|
||||||
# get distinct labels for all events
|
# get distinct labels for all events
|
||||||
distinct_labels = (
|
distinct_labels = (
|
||||||
@ -515,7 +512,7 @@ async def event_ids(ids: str, request: Request):
|
|||||||
def events_search(
|
def events_search(
|
||||||
request: Request,
|
request: Request,
|
||||||
params: EventsSearchQueryParams = Depends(),
|
params: EventsSearchQueryParams = Depends(),
|
||||||
allowed_cameras: List[str] = Depends(get_allowed_cameras_for_filter),
|
allowed_cameras: list[str] = Depends(get_allowed_cameras_for_filter),
|
||||||
):
|
):
|
||||||
query = params.query
|
query = params.query
|
||||||
search_type = params.search_type
|
search_type = params.search_type
|
||||||
@ -595,12 +592,12 @@ def events_search(
|
|||||||
filtered = requested.intersection(allowed_cameras)
|
filtered = requested.intersection(allowed_cameras)
|
||||||
if not filtered:
|
if not filtered:
|
||||||
return JSONResponse(content=[])
|
return JSONResponse(content=[])
|
||||||
event_filters.append((Event.camera << list(filtered)))
|
event_filters.append(Event.camera << list(filtered))
|
||||||
else:
|
else:
|
||||||
event_filters.append((Event.camera << allowed_cameras))
|
event_filters.append(Event.camera << allowed_cameras)
|
||||||
|
|
||||||
if labels != "all":
|
if labels != "all":
|
||||||
event_filters.append((Event.label << labels.split(",")))
|
event_filters.append(Event.label << labels.split(","))
|
||||||
|
|
||||||
if sub_labels != "all":
|
if sub_labels != "all":
|
||||||
# use matching so joined sub labels are included
|
# use matching so joined sub labels are included
|
||||||
@ -611,23 +608,23 @@ def events_search(
|
|||||||
|
|
||||||
if "None" in filtered_sub_labels:
|
if "None" in filtered_sub_labels:
|
||||||
filtered_sub_labels.remove("None")
|
filtered_sub_labels.remove("None")
|
||||||
sub_label_clauses.append((Event.sub_label.is_null()))
|
sub_label_clauses.append(Event.sub_label.is_null())
|
||||||
|
|
||||||
for label in filtered_sub_labels:
|
for label in filtered_sub_labels:
|
||||||
lowered = label.lower()
|
lowered = label.lower()
|
||||||
sub_label_clauses.append(
|
sub_label_clauses.append(
|
||||||
(fn.LOWER(Event.sub_label.cast("text")) == lowered)
|
fn.LOWER(Event.sub_label.cast("text")) == lowered
|
||||||
) # include exact matches (case-insensitive)
|
) # include exact matches (case-insensitive)
|
||||||
|
|
||||||
# include this label when part of a list (LIKE is case-insensitive in sqlite for ASCII)
|
# include this label when part of a list (LIKE is case-insensitive in sqlite for ASCII)
|
||||||
sub_label_clauses.append(
|
sub_label_clauses.append(
|
||||||
(fn.LOWER(Event.sub_label.cast("text")) % f"*{lowered},*")
|
fn.LOWER(Event.sub_label.cast("text")) % f"*{lowered},*"
|
||||||
)
|
)
|
||||||
sub_label_clauses.append(
|
sub_label_clauses.append(
|
||||||
(fn.LOWER(Event.sub_label.cast("text")) % f"*, {lowered}*")
|
fn.LOWER(Event.sub_label.cast("text")) % f"*, {lowered}*"
|
||||||
)
|
)
|
||||||
|
|
||||||
event_filters.append((reduce(operator.or_, sub_label_clauses)))
|
event_filters.append(reduce(operator.or_, sub_label_clauses))
|
||||||
|
|
||||||
if attributes != "all":
|
if attributes != "all":
|
||||||
# Custom classification results are stored as data[model_name] = result_value
|
# Custom classification results are stored as data[model_name] = result_value
|
||||||
@ -641,12 +638,12 @@ def events_search(
|
|||||||
|
|
||||||
if "None" in filtered_zones:
|
if "None" in filtered_zones:
|
||||||
filtered_zones.remove("None")
|
filtered_zones.remove("None")
|
||||||
zone_clauses.append((Event.zones.length() == 0))
|
zone_clauses.append(Event.zones.length() == 0)
|
||||||
|
|
||||||
for zone in filtered_zones:
|
for zone in filtered_zones:
|
||||||
zone_clauses.append((Event.zones.cast("text") % f'*"{zone}"*'))
|
zone_clauses.append(Event.zones.cast("text") % f'*"{zone}"*')
|
||||||
|
|
||||||
event_filters.append((reduce(operator.or_, zone_clauses)))
|
event_filters.append(reduce(operator.or_, zone_clauses))
|
||||||
|
|
||||||
if recognized_license_plate != "all":
|
if recognized_license_plate != "all":
|
||||||
filtered_recognized_license_plates = recognized_license_plate.split(",")
|
filtered_recognized_license_plates = recognized_license_plate.split(",")
|
||||||
@ -674,43 +671,43 @@ def events_search(
|
|||||||
)
|
)
|
||||||
|
|
||||||
recognized_license_plate_clause = reduce(operator.or_, clauses_for_plates)
|
recognized_license_plate_clause = reduce(operator.or_, clauses_for_plates)
|
||||||
event_filters.append((recognized_license_plate_clause))
|
event_filters.append(recognized_license_plate_clause)
|
||||||
|
|
||||||
if after:
|
if after:
|
||||||
event_filters.append((Event.start_time > after))
|
event_filters.append(Event.start_time > after)
|
||||||
|
|
||||||
if before:
|
if before:
|
||||||
event_filters.append((Event.start_time < before))
|
event_filters.append(Event.start_time < before)
|
||||||
|
|
||||||
if has_clip is not None:
|
if has_clip is not None:
|
||||||
event_filters.append((Event.has_clip == has_clip))
|
event_filters.append(Event.has_clip == has_clip)
|
||||||
|
|
||||||
if has_snapshot is not None:
|
if has_snapshot is not None:
|
||||||
event_filters.append((Event.has_snapshot == has_snapshot))
|
event_filters.append(Event.has_snapshot == has_snapshot)
|
||||||
|
|
||||||
if is_submitted is not None:
|
if is_submitted is not None:
|
||||||
if is_submitted == 0:
|
if is_submitted == 0:
|
||||||
event_filters.append((Event.plus_id.is_null()))
|
event_filters.append(Event.plus_id.is_null())
|
||||||
elif is_submitted > 0:
|
elif is_submitted > 0:
|
||||||
event_filters.append((Event.plus_id != ""))
|
event_filters.append(Event.plus_id != "")
|
||||||
|
|
||||||
if min_score is not None and max_score is not None:
|
if min_score is not None and max_score is not None:
|
||||||
event_filters.append((Event.data["score"].between(min_score, max_score)))
|
event_filters.append(Event.data["score"].between(min_score, max_score))
|
||||||
else:
|
else:
|
||||||
if min_score is not None:
|
if min_score is not None:
|
||||||
event_filters.append((Event.data["score"] >= min_score))
|
event_filters.append(Event.data["score"] >= min_score)
|
||||||
if max_score is not None:
|
if max_score is not None:
|
||||||
event_filters.append((Event.data["score"] <= max_score))
|
event_filters.append(Event.data["score"] <= max_score)
|
||||||
|
|
||||||
if min_speed is not None and max_speed is not None:
|
if min_speed is not None and max_speed is not None:
|
||||||
event_filters.append(
|
event_filters.append(
|
||||||
(Event.data["average_estimated_speed"].between(min_speed, max_speed))
|
Event.data["average_estimated_speed"].between(min_speed, max_speed)
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
if min_speed is not None:
|
if min_speed is not None:
|
||||||
event_filters.append((Event.data["average_estimated_speed"] >= min_speed))
|
event_filters.append(Event.data["average_estimated_speed"] >= min_speed)
|
||||||
if max_speed is not None:
|
if max_speed is not None:
|
||||||
event_filters.append((Event.data["average_estimated_speed"] <= max_speed))
|
event_filters.append(Event.data["average_estimated_speed"] <= max_speed)
|
||||||
|
|
||||||
if time_range != DEFAULT_TIME_RANGE:
|
if time_range != DEFAULT_TIME_RANGE:
|
||||||
tz_name = params.timezone
|
tz_name = params.timezone
|
||||||
@ -728,17 +725,15 @@ def events_search(
|
|||||||
# should use or operator
|
# should use or operator
|
||||||
if time_after > time_before:
|
if time_after > time_before:
|
||||||
event_filters.append(
|
event_filters.append(
|
||||||
(
|
|
||||||
reduce(
|
reduce(
|
||||||
operator.or_,
|
operator.or_,
|
||||||
[(start_hour_fun > time_after), (start_hour_fun < time_before)],
|
[(start_hour_fun > time_after), (start_hour_fun < time_before)],
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
)
|
|
||||||
# all other cases should be and operator
|
# all other cases should be and operator
|
||||||
else:
|
else:
|
||||||
event_filters.append((start_hour_fun > time_after))
|
event_filters.append(start_hour_fun > time_after)
|
||||||
event_filters.append((start_hour_fun < time_before))
|
event_filters.append(start_hour_fun < time_before)
|
||||||
|
|
||||||
# Perform semantic search
|
# Perform semantic search
|
||||||
search_results = {}
|
search_results = {}
|
||||||
@ -894,7 +889,7 @@ def events_search(
|
|||||||
@router.get("/events/summary", dependencies=[Depends(allow_any_authenticated())])
|
@router.get("/events/summary", dependencies=[Depends(allow_any_authenticated())])
|
||||||
def events_summary(
|
def events_summary(
|
||||||
params: EventsSummaryQueryParams = Depends(),
|
params: EventsSummaryQueryParams = Depends(),
|
||||||
allowed_cameras: List[str] = Depends(get_allowed_cameras_for_filter),
|
allowed_cameras: list[str] = Depends(get_allowed_cameras_for_filter),
|
||||||
):
|
):
|
||||||
tz_name = params.timezone
|
tz_name = params.timezone
|
||||||
has_clip = params.has_clip
|
has_clip = params.has_clip
|
||||||
@ -903,13 +898,13 @@ def events_summary(
|
|||||||
clauses = []
|
clauses = []
|
||||||
|
|
||||||
if has_clip is not None:
|
if has_clip is not None:
|
||||||
clauses.append((Event.has_clip == has_clip))
|
clauses.append(Event.has_clip == has_clip)
|
||||||
|
|
||||||
if has_snapshot is not None:
|
if has_snapshot is not None:
|
||||||
clauses.append((Event.has_snapshot == has_snapshot))
|
clauses.append(Event.has_snapshot == has_snapshot)
|
||||||
|
|
||||||
if len(clauses) == 0:
|
if len(clauses) == 0:
|
||||||
clauses.append((True))
|
clauses.append(True)
|
||||||
|
|
||||||
time_range_query = (
|
time_range_query = (
|
||||||
Event.select(
|
Event.select(
|
||||||
|
|||||||
@ -7,8 +7,8 @@ import string
|
|||||||
import time
|
import time
|
||||||
import zipfile
|
import zipfile
|
||||||
from collections import deque
|
from collections import deque
|
||||||
|
from collections.abc import Iterator
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Iterator, List, Optional
|
|
||||||
|
|
||||||
import psutil
|
import psutil
|
||||||
from fastapi import APIRouter, Depends, Query, Request
|
from fastapi import APIRouter, Depends, Query, Request
|
||||||
@ -89,7 +89,7 @@ def _generate_export_id(camera_name: str) -> str:
|
|||||||
|
|
||||||
def _create_export_case_record(
|
def _create_export_case_record(
|
||||||
name: str,
|
name: str,
|
||||||
description: Optional[str],
|
description: str | None,
|
||||||
) -> ExportCase:
|
) -> ExportCase:
|
||||||
now = datetime.datetime.fromtimestamp(time.time())
|
now = datetime.datetime.fromtimestamp(time.time())
|
||||||
return ExportCase.create(
|
return ExportCase.create(
|
||||||
@ -101,7 +101,7 @@ def _create_export_case_record(
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def _validate_camera_name(request: Request, camera_name: str) -> Optional[JSONResponse]:
|
def _validate_camera_name(request: Request, camera_name: str) -> JSONResponse | None:
|
||||||
if camera_name and request.app.frigate_config.cameras.get(camera_name):
|
if camera_name and request.app.frigate_config.cameras.get(camera_name):
|
||||||
return None
|
return None
|
||||||
|
|
||||||
@ -111,7 +111,7 @@ def _validate_camera_name(request: Request, camera_name: str) -> Optional[JSONRe
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def _validate_export_case(export_case_id: Optional[str]) -> Optional[JSONResponse]:
|
def _validate_export_case(export_case_id: str | None) -> JSONResponse | None:
|
||||||
if export_case_id is None:
|
if export_case_id is None:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
@ -127,8 +127,8 @@ def _validate_export_case(export_case_id: Optional[str]) -> Optional[JSONRespons
|
|||||||
|
|
||||||
|
|
||||||
def _sanitize_existing_image(
|
def _sanitize_existing_image(
|
||||||
image_path: Optional[str],
|
image_path: str | None,
|
||||||
) -> tuple[Optional[str], Optional[JSONResponse]]:
|
) -> tuple[str | None, JSONResponse | None]:
|
||||||
# sanitize_filepath normalizes "\" to "/" but leaves ".." intact, so a path
|
# sanitize_filepath normalizes "\" to "/" but leaves ".." intact, so a path
|
||||||
# like "clips\..\..\etc/passwd" passes the CLIPS_DIR prefix check yet still
|
# like "clips\..\..\etc/passwd" passes the CLIPS_DIR prefix check yet still
|
||||||
# escapes the directory once resolved. A valid snapshot path never uses "..".
|
# escapes the directory once resolved. A valid snapshot path never uses "..".
|
||||||
@ -154,7 +154,7 @@ def _validate_export_source(
|
|||||||
start_time: float,
|
start_time: float,
|
||||||
end_time: float,
|
end_time: float,
|
||||||
playback_source: PlaybackSourceEnum,
|
playback_source: PlaybackSourceEnum,
|
||||||
) -> Optional[str]:
|
) -> str | None:
|
||||||
if playback_source == PlaybackSourceEnum.recordings:
|
if playback_source == PlaybackSourceEnum.recordings:
|
||||||
recordings_count = (
|
recordings_count = (
|
||||||
Recordings.select()
|
Recordings.select()
|
||||||
@ -257,14 +257,14 @@ def _build_export_job(
|
|||||||
camera_name: str,
|
camera_name: str,
|
||||||
start_time: float,
|
start_time: float,
|
||||||
end_time: float,
|
end_time: float,
|
||||||
friendly_name: Optional[str],
|
friendly_name: str | None,
|
||||||
existing_image: Optional[str],
|
existing_image: str | None,
|
||||||
playback_source: PlaybackSourceEnum,
|
playback_source: PlaybackSourceEnum,
|
||||||
export_case_id: Optional[str],
|
export_case_id: str | None,
|
||||||
ffmpeg_input_args: Optional[str] = None,
|
ffmpeg_input_args: str | None = None,
|
||||||
ffmpeg_output_args: Optional[str] = None,
|
ffmpeg_output_args: str | None = None,
|
||||||
cpu_fallback: bool = False,
|
cpu_fallback: bool = False,
|
||||||
chapters: Optional[ChaptersEnum] = None,
|
chapters: ChaptersEnum | None = None,
|
||||||
) -> ExportJob:
|
) -> ExportJob:
|
||||||
return ExportJob(
|
return ExportJob(
|
||||||
id=_generate_export_id(camera_name),
|
id=_generate_export_id(camera_name),
|
||||||
@ -302,11 +302,11 @@ def _export_case_to_dict(case: ExportCase) -> dict[str, object]:
|
|||||||
Returns a list of exports ordered by date (most recent first).""",
|
Returns a list of exports ordered by date (most recent first).""",
|
||||||
)
|
)
|
||||||
def get_exports(
|
def get_exports(
|
||||||
allowed_cameras: List[str] = Depends(get_allowed_cameras_for_filter),
|
allowed_cameras: list[str] = Depends(get_allowed_cameras_for_filter),
|
||||||
export_case_id: Optional[str] = None,
|
export_case_id: str | None = None,
|
||||||
cameras: Optional[str] = Query(default="all"),
|
cameras: str | None = Query(default="all"),
|
||||||
start_date: Optional[float] = None,
|
start_date: float | None = None,
|
||||||
end_date: Optional[float] = None,
|
end_date: float | None = None,
|
||||||
):
|
):
|
||||||
query = Export.select().where(Export.camera << allowed_cameras)
|
query = Export.select().where(Export.camera << allowed_cameras)
|
||||||
|
|
||||||
@ -422,7 +422,7 @@ def _unique_archive_name(export: Export, used: set[str]) -> str:
|
|||||||
return candidate
|
return candidate
|
||||||
|
|
||||||
|
|
||||||
def _stream_case_archive(exports: List[Export]) -> Iterator[bytes]:
|
def _stream_case_archive(exports: list[Export]) -> Iterator[bytes]:
|
||||||
"""Yield bytes of a zip archive built from the given exports' mp4 files."""
|
"""Yield bytes of a zip archive built from the given exports' mp4 files."""
|
||||||
buffer = _StreamingZipBuffer()
|
buffer = _StreamingZipBuffer()
|
||||||
used_names: set[str] = set()
|
used_names: set[str] = set()
|
||||||
@ -466,7 +466,7 @@ def _stream_case_archive(exports: List[Export]) -> Iterator[bytes]:
|
|||||||
)
|
)
|
||||||
def download_export_case(
|
def download_export_case(
|
||||||
case_id: str,
|
case_id: str,
|
||||||
allowed_cameras: List[str] = Depends(get_allowed_cameras_for_filter),
|
allowed_cameras: list[str] = Depends(get_allowed_cameras_for_filter),
|
||||||
):
|
):
|
||||||
try:
|
try:
|
||||||
case = ExportCase.get(ExportCase.id == case_id)
|
case = ExportCase.get(ExportCase.id == case_id)
|
||||||
@ -580,7 +580,7 @@ def delete_export_case(case_id: str, request: Request, delete_exports: bool = Fa
|
|||||||
)
|
)
|
||||||
def get_active_export_jobs(
|
def get_active_export_jobs(
|
||||||
request: Request,
|
request: Request,
|
||||||
allowed_cameras: List[str] = Depends(get_allowed_cameras_for_filter),
|
allowed_cameras: list[str] = Depends(get_allowed_cameras_for_filter),
|
||||||
):
|
):
|
||||||
jobs = list_active_export_jobs(request.app.frigate_config)
|
jobs = list_active_export_jobs(request.app.frigate_config)
|
||||||
return JSONResponse(
|
return JSONResponse(
|
||||||
@ -622,7 +622,7 @@ async def get_export_job_status(export_id: str, request: Request):
|
|||||||
def export_recordings_batch(
|
def export_recordings_batch(
|
||||||
request: Request,
|
request: Request,
|
||||||
body: BatchExportBody,
|
body: BatchExportBody,
|
||||||
allowed_cameras: List[str] = Depends(get_allowed_cameras_for_filter),
|
allowed_cameras: list[str] = Depends(get_allowed_cameras_for_filter),
|
||||||
current_user: dict = Depends(get_current_user),
|
current_user: dict = Depends(get_current_user),
|
||||||
):
|
):
|
||||||
if isinstance(current_user, JSONResponse):
|
if isinstance(current_user, JSONResponse):
|
||||||
@ -662,7 +662,7 @@ def export_recordings_batch(
|
|||||||
|
|
||||||
# Sanitize each item's image_path up front. A bad path in any item
|
# Sanitize each item's image_path up front. A bad path in any item
|
||||||
# kills the whole request, consistent with single-export behavior.
|
# kills the whole request, consistent with single-export behavior.
|
||||||
sanitized_images: list[Optional[str]] = []
|
sanitized_images: list[str | None] = []
|
||||||
for item in body.items:
|
for item in body.items:
|
||||||
existing_image, image_validation_error = _sanitize_existing_image(
|
existing_image, image_validation_error = _sanitize_existing_image(
|
||||||
item.image_path
|
item.image_path
|
||||||
@ -713,7 +713,7 @@ def export_recordings_batch(
|
|||||||
export_case_id = export_case.id
|
export_case_id = export_case.id
|
||||||
|
|
||||||
export_ids: list[str] = []
|
export_ids: list[str] = []
|
||||||
results: list[dict[str, Optional[str] | bool | int]] = []
|
results: list[dict[str, str | None | bool | int]] = []
|
||||||
for index, item in enumerate(body.items):
|
for index, item in enumerate(body.items):
|
||||||
if index in item_errors:
|
if index in item_errors:
|
||||||
results.append(
|
results.append(
|
||||||
|
|||||||
@ -1,7 +1,6 @@
|
|||||||
import asyncio
|
import asyncio
|
||||||
import logging
|
import logging
|
||||||
import re
|
import re
|
||||||
from typing import Optional
|
|
||||||
|
|
||||||
from fastapi import Depends, FastAPI, Request
|
from fastapi import Depends, FastAPI, Request
|
||||||
from fastapi.responses import JSONResponse
|
from fastapi.responses import JSONResponse
|
||||||
@ -64,7 +63,7 @@ class RemoteUserPlugin(Plugin):
|
|||||||
def create_fastapi_app(
|
def create_fastapi_app(
|
||||||
frigate_config: FrigateConfig,
|
frigate_config: FrigateConfig,
|
||||||
database: SqliteQueueDatabase,
|
database: SqliteQueueDatabase,
|
||||||
embeddings: Optional[EmbeddingsContext],
|
embeddings: EmbeddingsContext | None,
|
||||||
detected_frames_processor,
|
detected_frames_processor,
|
||||||
storage_maintainer: StorageMaintainer,
|
storage_maintainer: StorageMaintainer,
|
||||||
onvif: OnvifController,
|
onvif: OnvifController,
|
||||||
@ -72,8 +71,8 @@ def create_fastapi_app(
|
|||||||
event_metadata_updater: EventMetadataPublisher,
|
event_metadata_updater: EventMetadataPublisher,
|
||||||
config_publisher: CameraConfigUpdatePublisher,
|
config_publisher: CameraConfigUpdatePublisher,
|
||||||
replay_manager: DebugReplayManager,
|
replay_manager: DebugReplayManager,
|
||||||
dispatcher: Optional[Dispatcher] = None,
|
dispatcher: Dispatcher | None = None,
|
||||||
profile_manager: Optional[ProfileManager] = None,
|
profile_manager: ProfileManager | None = None,
|
||||||
enforce_default_admin: bool = True,
|
enforce_default_admin: bool = True,
|
||||||
):
|
):
|
||||||
logger.info("Starting FastAPI app")
|
logger.info("Starting FastAPI app")
|
||||||
|
|||||||
@ -7,7 +7,7 @@ import math
|
|||||||
import os
|
import os
|
||||||
import subprocess as sp
|
import subprocess as sp
|
||||||
import time
|
import time
|
||||||
from datetime import datetime, timedelta, timezone
|
from datetime import UTC, datetime, timedelta
|
||||||
from pathlib import Path as FilePath
|
from pathlib import Path as FilePath
|
||||||
from typing import Any
|
from typing import Any
|
||||||
from urllib.parse import unquote
|
from urllib.parse import unquote
|
||||||
@ -314,11 +314,9 @@ async def get_snapshot_from_recording(
|
|||||||
Recordings.start_time,
|
Recordings.start_time,
|
||||||
)
|
)
|
||||||
.where(
|
.where(
|
||||||
(
|
|
||||||
(frame_time >= Recordings.start_time)
|
(frame_time >= Recordings.start_time)
|
||||||
& (frame_time <= Recordings.end_time)
|
& (frame_time <= Recordings.end_time)
|
||||||
)
|
)
|
||||||
)
|
|
||||||
.where(Recordings.camera == camera_name)
|
.where(Recordings.camera == camera_name)
|
||||||
.order_by(Recordings.start_time.desc())
|
.order_by(Recordings.start_time.desc())
|
||||||
.limit(1)
|
.limit(1)
|
||||||
@ -335,11 +333,9 @@ async def get_snapshot_from_recording(
|
|||||||
Recordings.start_time,
|
Recordings.start_time,
|
||||||
)
|
)
|
||||||
.where(
|
.where(
|
||||||
(
|
|
||||||
(frame_time >= Recordings.start_time)
|
(frame_time >= Recordings.start_time)
|
||||||
& (frame_time <= Recordings.end_time)
|
& (frame_time <= Recordings.end_time)
|
||||||
)
|
)
|
||||||
)
|
|
||||||
.where(Recordings.camera == camera_name)
|
.where(Recordings.camera == camera_name)
|
||||||
.order_by(Recordings.start_time.desc())
|
.order_by(Recordings.start_time.desc())
|
||||||
.limit(1)
|
.limit(1)
|
||||||
@ -398,10 +394,7 @@ async def submit_recording_snapshot_to_plus(
|
|||||||
Recordings.start_time,
|
Recordings.start_time,
|
||||||
)
|
)
|
||||||
.where(
|
.where(
|
||||||
(
|
(frame_time >= Recordings.start_time) & (frame_time <= Recordings.end_time)
|
||||||
(frame_time >= Recordings.start_time)
|
|
||||||
& (frame_time <= Recordings.end_time)
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
.where(Recordings.camera == camera_name)
|
.where(Recordings.camera == camera_name)
|
||||||
.order_by(Recordings.start_time.desc())
|
.order_by(Recordings.start_time.desc())
|
||||||
@ -719,7 +712,7 @@ async def vod_hour(
|
|||||||
):
|
):
|
||||||
parts = year_month.split("-")
|
parts = year_month.split("-")
|
||||||
start_date = (
|
start_date = (
|
||||||
datetime(int(parts[0]), int(parts[1]), day, hour, tzinfo=timezone.utc)
|
datetime(int(parts[0]), int(parts[1]), day, hour, tzinfo=UTC)
|
||||||
- datetime.now(pytz.timezone(tz_name.replace(",", "/"))).utcoffset()
|
- datetime.now(pytz.timezone(tz_name.replace(",", "/"))).utcoffset()
|
||||||
)
|
)
|
||||||
end_date = start_date + timedelta(hours=1) - timedelta(milliseconds=1)
|
end_date = start_date + timedelta(hours=1) - timedelta(milliseconds=1)
|
||||||
|
|||||||
@ -14,7 +14,6 @@ from __future__ import annotations
|
|||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
from enum import Enum
|
from enum import Enum
|
||||||
from typing import Optional
|
|
||||||
from urllib.parse import unquote, urlparse
|
from urllib.parse import unquote, urlparse
|
||||||
|
|
||||||
from peewee import DoesNotExist
|
from peewee import DoesNotExist
|
||||||
@ -42,7 +41,7 @@ class MediaAuthResolution(str, Enum):
|
|||||||
UNKNOWN = "unknown"
|
UNKNOWN = "unknown"
|
||||||
|
|
||||||
|
|
||||||
def extract_path(original_url: Optional[str]) -> Optional[str]:
|
def extract_path(original_url: str | None) -> str | None:
|
||||||
"""Return the decoded path component of nginx's `X-Original-URL` header.
|
"""Return the decoded path component of nginx's `X-Original-URL` header.
|
||||||
|
|
||||||
nginx forwards the *raw* request URI (with `..` segments intact) via
|
nginx forwards the *raw* request URI (with `..` segments intact) via
|
||||||
@ -72,8 +71,8 @@ def extract_path(original_url: Optional[str]) -> Optional[str]:
|
|||||||
|
|
||||||
|
|
||||||
def resolve_media_uri(
|
def resolve_media_uri(
|
||||||
uri: str, frigate_config: Optional[FrigateConfig] = None
|
uri: str, frigate_config: FrigateConfig | None = None
|
||||||
) -> tuple[MediaAuthResolution, Optional[str]]:
|
) -> tuple[MediaAuthResolution, str | None]:
|
||||||
"""Classify a URI and return the owning camera if applicable.
|
"""Classify a URI and return the owning camera if applicable.
|
||||||
|
|
||||||
`frigate_config` is used to disambiguate clip/review filenames whose
|
`frigate_config` is used to disambiguate clip/review filenames whose
|
||||||
@ -100,7 +99,7 @@ def resolve_media_uri(
|
|||||||
|
|
||||||
def _resolve_recording(
|
def _resolve_recording(
|
||||||
parts: list[str],
|
parts: list[str],
|
||||||
) -> tuple[MediaAuthResolution, Optional[str]]:
|
) -> tuple[MediaAuthResolution, str | None]:
|
||||||
# /recordings → neutral
|
# /recordings → neutral
|
||||||
# /recordings/{date} → neutral
|
# /recordings/{date} → neutral
|
||||||
# /recordings/{date}/{hour} → multi-camera listing
|
# /recordings/{date}/{hour} → multi-camera listing
|
||||||
@ -113,8 +112,8 @@ def _resolve_recording(
|
|||||||
|
|
||||||
|
|
||||||
def _resolve_clip(
|
def _resolve_clip(
|
||||||
parts: list[str], frigate_config: Optional[FrigateConfig]
|
parts: list[str], frigate_config: FrigateConfig | None
|
||||||
) -> tuple[MediaAuthResolution, Optional[str]]:
|
) -> tuple[MediaAuthResolution, str | None]:
|
||||||
# /clips → multi-camera listing
|
# /clips → multi-camera listing
|
||||||
# /clips/thumbs/{cam}/... → camera
|
# /clips/thumbs/{cam}/... → camera
|
||||||
# /clips/previews/{cam}/... → camera
|
# /clips/previews/{cam}/... → camera
|
||||||
@ -159,8 +158,8 @@ def _resolve_clip(
|
|||||||
|
|
||||||
|
|
||||||
def _longest_prefix_camera(
|
def _longest_prefix_camera(
|
||||||
stem: str, frigate_config: Optional[FrigateConfig]
|
stem: str, frigate_config: FrigateConfig | None
|
||||||
) -> Optional[str]:
|
) -> str | None:
|
||||||
if frigate_config is None:
|
if frigate_config is None:
|
||||||
return None
|
return None
|
||||||
for cam in sorted(frigate_config.cameras.keys(), key=len, reverse=True):
|
for cam in sorted(frigate_config.cameras.keys(), key=len, reverse=True):
|
||||||
@ -170,8 +169,8 @@ def _longest_prefix_camera(
|
|||||||
|
|
||||||
|
|
||||||
def _camera_from_clip_filename(
|
def _camera_from_clip_filename(
|
||||||
filename: str, frigate_config: Optional[FrigateConfig]
|
filename: str, frigate_config: FrigateConfig | None
|
||||||
) -> Optional[str]:
|
) -> str | None:
|
||||||
"""Match a flat clip filename `{camera}-{event_id}[-clean].{ext}` against
|
"""Match a flat clip filename `{camera}-{event_id}[-clean].{ext}` against
|
||||||
configured camera names. Longest-prefix wins so camera names containing
|
configured camera names. Longest-prefix wins so camera names containing
|
||||||
hyphens (e.g. `front-door`) resolve correctly.
|
hyphens (e.g. `front-door`) resolve correctly.
|
||||||
@ -182,8 +181,8 @@ def _camera_from_clip_filename(
|
|||||||
|
|
||||||
|
|
||||||
def _camera_from_thumb_filename(
|
def _camera_from_thumb_filename(
|
||||||
filename: str, frigate_config: Optional[FrigateConfig]
|
filename: str, frigate_config: FrigateConfig | None
|
||||||
) -> Optional[str]:
|
) -> str | None:
|
||||||
"""Match a review thumbnail filename `thumb-{camera}-{review_id}.webp`."""
|
"""Match a review thumbnail filename `thumb-{camera}-{review_id}.webp`."""
|
||||||
if not filename.startswith("thumb-"):
|
if not filename.startswith("thumb-"):
|
||||||
return None
|
return None
|
||||||
@ -194,7 +193,7 @@ def _camera_from_thumb_filename(
|
|||||||
|
|
||||||
def _resolve_export(
|
def _resolve_export(
|
||||||
parts: list[str],
|
parts: list[str],
|
||||||
) -> tuple[MediaAuthResolution, Optional[str]]:
|
) -> tuple[MediaAuthResolution, str | None]:
|
||||||
# /exports → multi-camera listing
|
# /exports → multi-camera listing
|
||||||
# /exports/{filename}.mp4 → camera (DB lookup by exact path)
|
# /exports/{filename}.mp4 → camera (DB lookup by exact path)
|
||||||
if len(parts) == 1:
|
if len(parts) == 1:
|
||||||
@ -240,8 +239,8 @@ def is_role_restricted(role: str, frigate_config: FrigateConfig) -> bool:
|
|||||||
|
|
||||||
|
|
||||||
def deny_response_for_media_uri(
|
def deny_response_for_media_uri(
|
||||||
original_url: Optional[str], role: Optional[str], frigate_config: FrigateConfig
|
original_url: str | None, role: str | None, frigate_config: FrigateConfig
|
||||||
) -> Optional[int]:
|
) -> int | None:
|
||||||
"""Decide whether the current role should be blocked from `original_url`.
|
"""Decide whether the current role should be blocked from `original_url`.
|
||||||
|
|
||||||
Returns an HTTP status code (403) when access should be denied, or `None`
|
Returns an HTTP status code (403) when access should be denied, or `None`
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
"""Motion search API for detecting changes within a region of interest."""
|
"""Motion search API for detecting changes within a region of interest."""
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
from typing import Any, List, Optional
|
from typing import Any
|
||||||
|
|
||||||
from fastapi import APIRouter, Depends, Request
|
from fastapi import APIRouter, Depends, Request
|
||||||
from fastapi.responses import JSONResponse
|
from fastapi.responses import JSONResponse
|
||||||
@ -26,7 +26,7 @@ class MotionSearchRequest(BaseModel):
|
|||||||
|
|
||||||
start_time: float = Field(description="Start timestamp for the search range")
|
start_time: float = Field(description="Start timestamp for the search range")
|
||||||
end_time: float = Field(description="End timestamp for the search range")
|
end_time: float = Field(description="End timestamp for the search range")
|
||||||
polygon_points: List[List[float]] = Field(
|
polygon_points: list[list[float]] = Field(
|
||||||
description="List of [x, y] normalized coordinates (0-1) defining the ROI polygon"
|
description="List of [x, y] normalized coordinates (0-1) defining the ROI polygon"
|
||||||
)
|
)
|
||||||
threshold: int = Field(
|
threshold: int = Field(
|
||||||
@ -87,12 +87,12 @@ class MotionSearchStatusResponse(BaseModel):
|
|||||||
success: bool
|
success: bool
|
||||||
message: str
|
message: str
|
||||||
status: str # "queued", "running", "success", "failed", or "cancelled"
|
status: str # "queued", "running", "success", "failed", or "cancelled"
|
||||||
results: Optional[List[MotionSearchResult]] = None
|
results: list[MotionSearchResult] | None = None
|
||||||
total_frames_processed: Optional[int] = None
|
total_frames_processed: int | None = None
|
||||||
error_message: Optional[str] = None
|
error_message: str | None = None
|
||||||
metrics: Optional[MotionSearchMetricsResponse] = None
|
metrics: MotionSearchMetricsResponse | None = None
|
||||||
scanning_timestamp: Optional[float] = None
|
scanning_timestamp: float | None = None
|
||||||
progress: Optional[float] = None
|
progress: float | None = None
|
||||||
|
|
||||||
|
|
||||||
@router.post(
|
@router.post(
|
||||||
|
|||||||
@ -4,7 +4,7 @@ import bisect
|
|||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
import threading
|
import threading
|
||||||
from datetime import datetime, timedelta, timezone
|
from datetime import UTC, datetime, timedelta
|
||||||
|
|
||||||
import pytz
|
import pytz
|
||||||
from fastapi import APIRouter, Depends, HTTPException
|
from fastapi import APIRouter, Depends, HTTPException
|
||||||
@ -125,7 +125,7 @@ def preview_hour(
|
|||||||
"""Get all mp4 previews relevant for time period given the timezone"""
|
"""Get all mp4 previews relevant for time period given the timezone"""
|
||||||
parts = year_month.split("-")
|
parts = year_month.split("-")
|
||||||
start_date = (
|
start_date = (
|
||||||
datetime(int(parts[0]), int(parts[1]), int(day), int(hour), tzinfo=timezone.utc)
|
datetime(int(parts[0]), int(parts[1]), int(day), int(hour), tzinfo=UTC)
|
||||||
- datetime.now(pytz.timezone(tz_name.replace(",", "/"))).utcoffset()
|
- datetime.now(pytz.timezone(tz_name.replace(",", "/"))).utcoffset()
|
||||||
)
|
)
|
||||||
end_date = start_date + timedelta(hours=1) - timedelta(milliseconds=1)
|
end_date = start_date + timedelta(hours=1) - timedelta(milliseconds=1)
|
||||||
|
|||||||
@ -5,7 +5,6 @@ import logging
|
|||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
from functools import reduce
|
from functools import reduce
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import List
|
|
||||||
from urllib.parse import unquote
|
from urllib.parse import unquote
|
||||||
|
|
||||||
from fastapi import APIRouter, Depends, Request
|
from fastapi import APIRouter, Depends, Request
|
||||||
@ -63,7 +62,7 @@ def get_recordings_storage_usage(request: Request):
|
|||||||
def all_recordings_summary(
|
def all_recordings_summary(
|
||||||
request: Request,
|
request: Request,
|
||||||
params: MediaRecordingsSummaryQueryParams = Depends(),
|
params: MediaRecordingsSummaryQueryParams = Depends(),
|
||||||
allowed_cameras: List[str] = Depends(get_allowed_cameras_for_filter),
|
allowed_cameras: list[str] = Depends(get_allowed_cameras_for_filter),
|
||||||
):
|
):
|
||||||
"""Returns true/false by day indicating if recordings exist"""
|
"""Returns true/false by day indicating if recordings exist"""
|
||||||
|
|
||||||
@ -263,7 +262,7 @@ async def recordings(
|
|||||||
async def no_recordings(
|
async def no_recordings(
|
||||||
request: Request,
|
request: Request,
|
||||||
params: MediaRecordingsAvailabilityQueryParams = Depends(),
|
params: MediaRecordingsAvailabilityQueryParams = Depends(),
|
||||||
allowed_cameras: List[str] = Depends(get_allowed_cameras_for_filter),
|
allowed_cameras: list[str] = Depends(get_allowed_cameras_for_filter),
|
||||||
):
|
):
|
||||||
"""Get time ranges with no recordings."""
|
"""Get time ranges with no recordings."""
|
||||||
cameras = params.cameras
|
cameras = params.cameras
|
||||||
@ -365,7 +364,7 @@ async def delete_recordings(
|
|||||||
start: float = PathParam(..., description="Start timestamp (unix)"),
|
start: float = PathParam(..., description="Start timestamp (unix)"),
|
||||||
end: float = PathParam(..., description="End timestamp (unix)"),
|
end: float = PathParam(..., description="End timestamp (unix)"),
|
||||||
params: RecordingsDeleteQueryParams = Depends(),
|
params: RecordingsDeleteQueryParams = Depends(),
|
||||||
allowed_cameras: List[str] = Depends(get_allowed_cameras_for_filter),
|
allowed_cameras: list[str] = Depends(get_allowed_cameras_for_filter),
|
||||||
):
|
):
|
||||||
"""Delete recordings in the specified time range."""
|
"""Delete recordings in the specified time range."""
|
||||||
if start >= end:
|
if start >= end:
|
||||||
|
|||||||
@ -4,7 +4,6 @@ import datetime
|
|||||||
import logging
|
import logging
|
||||||
from functools import reduce
|
from functools import reduce
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import List
|
|
||||||
|
|
||||||
import pandas as pd
|
import pandas as pd
|
||||||
from fastapi import APIRouter, Request
|
from fastapi import APIRouter, Request
|
||||||
@ -51,7 +50,7 @@ router = APIRouter(tags=[Tags.review])
|
|||||||
async def review(
|
async def review(
|
||||||
params: ReviewQueryParams = Depends(),
|
params: ReviewQueryParams = Depends(),
|
||||||
current_user: dict = Depends(get_current_user),
|
current_user: dict = Depends(get_current_user),
|
||||||
allowed_cameras: List[str] = Depends(get_allowed_cameras_for_filter),
|
allowed_cameras: list[str] = Depends(get_allowed_cameras_for_filter),
|
||||||
):
|
):
|
||||||
if isinstance(current_user, JSONResponse):
|
if isinstance(current_user, JSONResponse):
|
||||||
return current_user
|
return current_user
|
||||||
@ -83,7 +82,7 @@ async def review(
|
|||||||
camera_list = list(filtered)
|
camera_list = list(filtered)
|
||||||
else:
|
else:
|
||||||
camera_list = allowed_cameras
|
camera_list = allowed_cameras
|
||||||
clauses.append((ReviewSegment.camera << camera_list))
|
clauses.append(ReviewSegment.camera << camera_list)
|
||||||
|
|
||||||
if labels != "all":
|
if labels != "all":
|
||||||
# use matching so segments with multiple labels
|
# use matching so segments with multiple labels
|
||||||
@ -106,12 +105,12 @@ async def review(
|
|||||||
|
|
||||||
for zone in filtered_zones:
|
for zone in filtered_zones:
|
||||||
zone_clauses.append(
|
zone_clauses.append(
|
||||||
(ReviewSegment.data["zones"].cast("text") % f'*"{zone}"*')
|
ReviewSegment.data["zones"].cast("text") % f'*"{zone}"*'
|
||||||
)
|
)
|
||||||
clauses.append(reduce(operator.or_, zone_clauses))
|
clauses.append(reduce(operator.or_, zone_clauses))
|
||||||
|
|
||||||
if severity:
|
if severity:
|
||||||
clauses.append((ReviewSegment.severity == severity))
|
clauses.append(ReviewSegment.severity == severity)
|
||||||
|
|
||||||
# Join with UserReviewStatus to get per-user review status
|
# Join with UserReviewStatus to get per-user review status
|
||||||
review_query = (
|
review_query = (
|
||||||
@ -204,7 +203,7 @@ async def review_ids(request: Request, ids: str):
|
|||||||
async def review_summary(
|
async def review_summary(
|
||||||
params: ReviewSummaryQueryParams = Depends(),
|
params: ReviewSummaryQueryParams = Depends(),
|
||||||
current_user: dict = Depends(get_current_user),
|
current_user: dict = Depends(get_current_user),
|
||||||
allowed_cameras: List[str] = Depends(get_allowed_cameras_for_filter),
|
allowed_cameras: list[str] = Depends(get_allowed_cameras_for_filter),
|
||||||
):
|
):
|
||||||
if isinstance(current_user, JSONResponse):
|
if isinstance(current_user, JSONResponse):
|
||||||
return current_user
|
return current_user
|
||||||
@ -227,7 +226,7 @@ async def review_summary(
|
|||||||
camera_list = list(filtered)
|
camera_list = list(filtered)
|
||||||
else:
|
else:
|
||||||
camera_list = allowed_cameras
|
camera_list = allowed_cameras
|
||||||
clauses.append((ReviewSegment.camera << camera_list))
|
clauses.append(ReviewSegment.camera << camera_list)
|
||||||
|
|
||||||
if labels != "all":
|
if labels != "all":
|
||||||
# use matching so segments with multiple labels
|
# use matching so segments with multiple labels
|
||||||
@ -328,7 +327,7 @@ async def review_summary(
|
|||||||
camera_list = list(filtered)
|
camera_list = list(filtered)
|
||||||
else:
|
else:
|
||||||
camera_list = allowed_cameras
|
camera_list = allowed_cameras
|
||||||
clauses.append((ReviewSegment.camera << camera_list))
|
clauses.append(ReviewSegment.camera << camera_list)
|
||||||
|
|
||||||
if labels != "all":
|
if labels != "all":
|
||||||
# use matching so segments with multiple labels
|
# use matching so segments with multiple labels
|
||||||
@ -584,7 +583,7 @@ def delete_reviews(body: ReviewModifyMultipleBody):
|
|||||||
)
|
)
|
||||||
def motion_activity(
|
def motion_activity(
|
||||||
params: ReviewActivityMotionQueryParams = Depends(),
|
params: ReviewActivityMotionQueryParams = Depends(),
|
||||||
allowed_cameras: List[str] = Depends(get_allowed_cameras_for_filter),
|
allowed_cameras: list[str] = Depends(get_allowed_cameras_for_filter),
|
||||||
):
|
):
|
||||||
"""Get motion and audio activity."""
|
"""Get motion and audio activity."""
|
||||||
cameras = params.cameras
|
cameras = params.cameras
|
||||||
@ -597,7 +596,7 @@ def motion_activity(
|
|||||||
scale = params.scale
|
scale = params.scale
|
||||||
|
|
||||||
clauses = [(Recordings.start_time > after) & (Recordings.end_time < before)]
|
clauses = [(Recordings.start_time > after) & (Recordings.end_time < before)]
|
||||||
clauses.append((Recordings.motion > 0))
|
clauses.append(Recordings.motion > 0)
|
||||||
|
|
||||||
if cameras != "all":
|
if cameras != "all":
|
||||||
requested = set(cameras.split(","))
|
requested = set(cameras.split(","))
|
||||||
@ -608,7 +607,7 @@ def motion_activity(
|
|||||||
else:
|
else:
|
||||||
camera_list = list(allowed_cameras)
|
camera_list = list(allowed_cameras)
|
||||||
|
|
||||||
clauses.append((Recordings.camera << camera_list))
|
clauses.append(Recordings.camera << camera_list)
|
||||||
|
|
||||||
data: list[Recordings] = (
|
data: list[Recordings] = (
|
||||||
Recordings.select(
|
Recordings.select(
|
||||||
|
|||||||
@ -4,11 +4,11 @@ import multiprocessing as mp
|
|||||||
import os
|
import os
|
||||||
import secrets
|
import secrets
|
||||||
import shutil
|
import shutil
|
||||||
|
from collections.abc import Callable
|
||||||
from multiprocessing import Queue
|
from multiprocessing import Queue
|
||||||
from multiprocessing.managers import DictProxy, SyncManager
|
from multiprocessing.managers import DictProxy, SyncManager
|
||||||
from multiprocessing.synchronize import Event as MpEvent
|
from multiprocessing.synchronize import Event as MpEvent
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Callable, Optional
|
|
||||||
|
|
||||||
import psutil
|
import psutil
|
||||||
import uvicorn
|
import uvicorn
|
||||||
@ -95,7 +95,7 @@ class FrigateApp:
|
|||||||
self, config: FrigateConfig, manager: SyncManager, stop_event: MpEvent
|
self, config: FrigateConfig, manager: SyncManager, stop_event: MpEvent
|
||||||
) -> None:
|
) -> None:
|
||||||
self.metrics_manager = manager
|
self.metrics_manager = manager
|
||||||
self.audio_process: Optional[mp.Process] = None
|
self.audio_process: mp.Process | None = None
|
||||||
self.stop_event = stop_event
|
self.stop_event = stop_event
|
||||||
self.detection_queue: Queue = mp.Queue()
|
self.detection_queue: Queue = mp.Queue()
|
||||||
self.detectors: dict[str, ObjectDetectProcess] = {}
|
self.detectors: dict[str, ObjectDetectProcess] = {}
|
||||||
@ -120,8 +120,8 @@ class FrigateApp:
|
|||||||
)
|
)
|
||||||
self.ptz_metrics: dict[str, PTZMetrics] = {}
|
self.ptz_metrics: dict[str, PTZMetrics] = {}
|
||||||
self.processes: dict[str, int] = {}
|
self.processes: dict[str, int] = {}
|
||||||
self.embeddings: Optional[EmbeddingsContext] = None
|
self.embeddings: EmbeddingsContext | None = None
|
||||||
self.profile_manager: Optional[ProfileManager] = None
|
self.profile_manager: ProfileManager | None = None
|
||||||
self.config = config
|
self.config = config
|
||||||
|
|
||||||
def ensure_dirs(self) -> None:
|
def ensure_dirs(self) -> None:
|
||||||
|
|||||||
@ -6,7 +6,8 @@ import logging
|
|||||||
import random
|
import random
|
||||||
import string
|
import string
|
||||||
from collections import Counter
|
from collections import Counter
|
||||||
from typing import Any, Callable
|
from collections.abc import Callable
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
from frigate.comms.event_metadata_updater import (
|
from frigate.comms.event_metadata_updater import (
|
||||||
EventMetadataPublisher,
|
EventMetadataPublisher,
|
||||||
|
|||||||
@ -5,7 +5,8 @@ import logging
|
|||||||
import os
|
import os
|
||||||
import threading
|
import threading
|
||||||
from collections import defaultdict
|
from collections import defaultdict
|
||||||
from typing import Any, Callable
|
from collections.abc import Callable
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
import cv2
|
import cv2
|
||||||
import numpy as np
|
import numpy as np
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
from abc import ABC, abstractmethod
|
from abc import ABC, abstractmethod
|
||||||
from typing import Any, Callable
|
from collections.abc import Callable
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
|
||||||
class Communicator(ABC):
|
class Communicator(ABC):
|
||||||
|
|||||||
@ -3,8 +3,8 @@
|
|||||||
import datetime
|
import datetime
|
||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
from collections.abc import Iterable
|
from collections.abc import Callable, Iterable
|
||||||
from typing import Any, Callable, Optional, cast
|
from typing import Any, cast
|
||||||
|
|
||||||
from frigate.camera import PTZMetrics
|
from frigate.camera import PTZMetrics
|
||||||
from frigate.camera.activity_manager import AudioActivityManager, CameraActivityManager
|
from frigate.camera.activity_manager import AudioActivityManager, CameraActivityManager
|
||||||
@ -97,7 +97,7 @@ class Dispatcher:
|
|||||||
"notifications": self._on_global_notification_command,
|
"notifications": self._on_global_notification_command,
|
||||||
"profile": self._on_profile_command,
|
"profile": self._on_profile_command,
|
||||||
}
|
}
|
||||||
self.profile_manager: Optional[ProfileManager] = None
|
self.profile_manager: ProfileManager | None = None
|
||||||
|
|
||||||
for comm in self.comms:
|
for comm in self.comms:
|
||||||
comm.subscribe(self._receive)
|
comm.subscribe(self._receive)
|
||||||
@ -106,7 +106,7 @@ class Dispatcher:
|
|||||||
(comm for comm in communicators if isinstance(comm, WebPushClient)), None
|
(comm for comm in communicators if isinstance(comm, WebPushClient)), None
|
||||||
)
|
)
|
||||||
|
|
||||||
def _receive(self, topic: str, payload: Any) -> Optional[Any]:
|
def _receive(self, topic: str, payload: Any) -> Any | None:
|
||||||
"""Handle receiving of payload from communicators."""
|
"""Handle receiving of payload from communicators."""
|
||||||
|
|
||||||
def handle_camera_command(
|
def handle_camera_command(
|
||||||
|
|||||||
@ -1,8 +1,9 @@
|
|||||||
"""Facilitates communication between processes."""
|
"""Facilitates communication between processes."""
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
|
from collections.abc import Callable
|
||||||
from enum import Enum
|
from enum import Enum
|
||||||
from typing import Any, Callable
|
from typing import Any
|
||||||
|
|
||||||
import zmq
|
import zmq
|
||||||
|
|
||||||
|
|||||||
@ -3,8 +3,9 @@
|
|||||||
import logging
|
import logging
|
||||||
import multiprocessing as mp
|
import multiprocessing as mp
|
||||||
import threading
|
import threading
|
||||||
|
from collections.abc import Callable
|
||||||
from multiprocessing.synchronize import Event as MpEvent
|
from multiprocessing.synchronize import Event as MpEvent
|
||||||
from typing import Any, Callable
|
from typing import Any
|
||||||
|
|
||||||
import zmq
|
import zmq
|
||||||
|
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
import logging
|
import logging
|
||||||
import threading
|
import threading
|
||||||
from typing import Any, Callable
|
from collections.abc import Callable
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
import paho.mqtt.client as mqtt
|
import paho.mqtt.client as mqtt
|
||||||
from paho.mqtt.enums import CallbackAPIVersion
|
from paho.mqtt.enums import CallbackAPIVersion
|
||||||
@ -214,8 +215,8 @@ class MqttClient(Communicator):
|
|||||||
logger.error("Unable to connect to MQTT server: MQTT Not authorized")
|
logger.error("Unable to connect to MQTT server: MQTT Not authorized")
|
||||||
else:
|
else:
|
||||||
logger.error(
|
logger.error(
|
||||||
"Unable to connect to MQTT server: Connection refused. Error code: "
|
"Unable to connect to MQTT server: Connection refused. Error code: %s",
|
||||||
+ reason_code.getName()
|
reason_code.getName(),
|
||||||
)
|
)
|
||||||
|
|
||||||
self.connected = True
|
self.connected = True
|
||||||
|
|||||||
@ -146,7 +146,7 @@ class RuntimeStatePersistence:
|
|||||||
if not os.path.exists(self._path):
|
if not os.path.exists(self._path):
|
||||||
return {}
|
return {}
|
||||||
try:
|
try:
|
||||||
with open(self._path, "r") as f:
|
with open(self._path) as f:
|
||||||
data = json.load(f)
|
data = json.load(f)
|
||||||
except (OSError, json.JSONDecodeError):
|
except (OSError, json.JSONDecodeError):
|
||||||
logger.exception(
|
logger.exception(
|
||||||
|
|||||||
@ -6,9 +6,10 @@ import logging
|
|||||||
import os
|
import os
|
||||||
import queue
|
import queue
|
||||||
import threading
|
import threading
|
||||||
|
from collections.abc import Callable
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from multiprocessing.synchronize import Event as MpEvent
|
from multiprocessing.synchronize import Event as MpEvent
|
||||||
from typing import Any, Callable
|
from typing import Any
|
||||||
|
|
||||||
from py_vapid import Vapid01
|
from py_vapid import Vapid01
|
||||||
from pywebpush import WebPusher
|
from pywebpush import WebPusher
|
||||||
|
|||||||
@ -4,7 +4,8 @@ import errno
|
|||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
import threading
|
import threading
|
||||||
from typing import Any, Callable
|
from collections.abc import Callable
|
||||||
|
from typing import Any
|
||||||
from wsgiref.simple_server import make_server
|
from wsgiref.simple_server import make_server
|
||||||
|
|
||||||
from ws4py.server.wsgirefserver import (
|
from ws4py.server.wsgirefserver import (
|
||||||
|
|||||||
@ -1,5 +1,3 @@
|
|||||||
from typing import Dict, List, Optional
|
|
||||||
|
|
||||||
from pydantic import Field, field_validator, model_validator
|
from pydantic import Field, field_validator, model_validator
|
||||||
|
|
||||||
from .base import FrigateBaseModel
|
from .base import FrigateBaseModel
|
||||||
@ -41,7 +39,7 @@ class AuthConfig(FrigateBaseModel):
|
|||||||
description="When a session is within this many seconds of expiring, refresh it back to full length.",
|
description="When a session is within this many seconds of expiring, refresh it back to full length.",
|
||||||
ge=30,
|
ge=30,
|
||||||
)
|
)
|
||||||
failed_login_rate_limit: Optional[str] = Field(
|
failed_login_rate_limit: str | None = Field(
|
||||||
default=None,
|
default=None,
|
||||||
title="Failed login limits",
|
title="Failed login limits",
|
||||||
description="Rate limiting rules for failed login attempts to reduce brute-force attacks.",
|
description="Rate limiting rules for failed login attempts to reduce brute-force attacks.",
|
||||||
@ -57,12 +55,12 @@ class AuthConfig(FrigateBaseModel):
|
|||||||
title="Hash iterations",
|
title="Hash iterations",
|
||||||
description="Number of PBKDF2-SHA256 iterations to use when hashing user passwords.",
|
description="Number of PBKDF2-SHA256 iterations to use when hashing user passwords.",
|
||||||
)
|
)
|
||||||
roles: Dict[str, List[str]] = Field(
|
roles: dict[str, list[str]] = Field(
|
||||||
default_factory=dict,
|
default_factory=dict,
|
||||||
title="Role mappings",
|
title="Role mappings",
|
||||||
description="Map roles to camera lists. An empty list grants access to all cameras for the role.",
|
description="Map roles to camera lists. An empty list grants access to all cameras for the role.",
|
||||||
)
|
)
|
||||||
admin_first_time_login: Optional[bool] = Field(
|
admin_first_time_login: bool | None = Field(
|
||||||
default=False,
|
default=False,
|
||||||
title="First-time admin flag",
|
title="First-time admin flag",
|
||||||
description=(
|
description=(
|
||||||
@ -72,7 +70,7 @@ class AuthConfig(FrigateBaseModel):
|
|||||||
|
|
||||||
@field_validator("roles")
|
@field_validator("roles")
|
||||||
@classmethod
|
@classmethod
|
||||||
def validate_roles(cls, v: Dict[str, List[str]]) -> Dict[str, List[str]]:
|
def validate_roles(cls, v: dict[str, list[str]]) -> dict[str, list[str]]:
|
||||||
# Ensure role names are valid (alphanumeric with underscores)
|
# Ensure role names are valid (alphanumeric with underscores)
|
||||||
for role in v.keys():
|
for role in v.keys():
|
||||||
if not role.replace("_", "").isalnum():
|
if not role.replace("_", "").isalnum():
|
||||||
|
|||||||
@ -1,5 +1,3 @@
|
|||||||
from typing import Optional
|
|
||||||
|
|
||||||
from pydantic import Field
|
from pydantic import Field
|
||||||
|
|
||||||
from frigate.const import AUDIO_MIN_CONFIDENCE
|
from frigate.const import AUDIO_MIN_CONFIDENCE
|
||||||
@ -43,12 +41,12 @@ class AudioConfig(FrigateBaseModel):
|
|||||||
title="Listen types",
|
title="Listen types",
|
||||||
description="List of audio event types to detect (for example: bark, fire_alarm, speech, yell).",
|
description="List of audio event types to detect (for example: bark, fire_alarm, speech, yell).",
|
||||||
)
|
)
|
||||||
filters: Optional[dict[str, AudioFilterConfig]] = Field(
|
filters: dict[str, AudioFilterConfig] | None = Field(
|
||||||
None,
|
None,
|
||||||
title="Audio filters",
|
title="Audio filters",
|
||||||
description="Per-audio-type filter settings such as confidence thresholds used to reduce false positives.",
|
description="Per-audio-type filter settings such as confidence thresholds used to reduce false positives.",
|
||||||
)
|
)
|
||||||
enabled_in_config: Optional[bool] = Field(
|
enabled_in_config: bool | None = Field(
|
||||||
None,
|
None,
|
||||||
title="Original audio state",
|
title="Original audio state",
|
||||||
description="Indicates whether audio detection was originally enabled in the static config file.",
|
description="Indicates whether audio detection was originally enabled in the static config file.",
|
||||||
|
|||||||
@ -1,5 +1,4 @@
|
|||||||
from enum import Enum
|
from enum import Enum
|
||||||
from typing import Optional
|
|
||||||
|
|
||||||
from pydantic import BaseModel, Field
|
from pydantic import BaseModel, Field
|
||||||
|
|
||||||
@ -35,7 +34,7 @@ class BirdseyeLayoutConfig(FrigateBaseModel):
|
|||||||
ge=1.0,
|
ge=1.0,
|
||||||
le=5.0,
|
le=5.0,
|
||||||
)
|
)
|
||||||
max_cameras: Optional[int] = Field(
|
max_cameras: int | None = Field(
|
||||||
default=None,
|
default=None,
|
||||||
title="Max cameras",
|
title="Max cameras",
|
||||||
description="Maximum number of cameras to display at once in Birdseye; shows the most recent cameras.",
|
description="Maximum number of cameras to display at once in Birdseye; shows the most recent cameras.",
|
||||||
|
|||||||
@ -1,6 +1,5 @@
|
|||||||
import os
|
import os
|
||||||
from enum import Enum
|
from enum import Enum
|
||||||
from typing import Optional
|
|
||||||
|
|
||||||
from pydantic import Field, PrivateAttr, model_validator
|
from pydantic import Field, PrivateAttr, model_validator
|
||||||
|
|
||||||
@ -51,14 +50,14 @@ class CameraTypeEnum(str, Enum):
|
|||||||
|
|
||||||
|
|
||||||
class CameraConfig(FrigateBaseModel):
|
class CameraConfig(FrigateBaseModel):
|
||||||
name: Optional[str] = Field(
|
name: str | None = Field(
|
||||||
None,
|
None,
|
||||||
title="Camera name",
|
title="Camera name",
|
||||||
description="Camera name is required",
|
description="Camera name is required",
|
||||||
pattern=REGEX_CAMERA_NAME,
|
pattern=REGEX_CAMERA_NAME,
|
||||||
)
|
)
|
||||||
|
|
||||||
friendly_name: Optional[str] = Field(
|
friendly_name: str | None = Field(
|
||||||
None,
|
None,
|
||||||
title="Friendly name",
|
title="Friendly name",
|
||||||
description="Camera friendly name used in the Frigate UI",
|
description="Camera friendly name used in the Frigate UI",
|
||||||
@ -180,7 +179,7 @@ class CameraConfig(FrigateBaseModel):
|
|||||||
title="Camera UI",
|
title="Camera UI",
|
||||||
description="Display ordering and visibility for this camera in the UI. Ordering affects the default dashboard. For more granular control, use camera groups.",
|
description="Display ordering and visibility for this camera in the UI. Ordering affects the default dashboard. For more granular control, use camera groups.",
|
||||||
)
|
)
|
||||||
webui_url: Optional[str] = Field(
|
webui_url: str | None = Field(
|
||||||
None,
|
None,
|
||||||
title="Camera URL",
|
title="Camera URL",
|
||||||
description="URL to visit the camera directly from system page",
|
description="URL to visit the camera directly from system page",
|
||||||
@ -196,7 +195,7 @@ class CameraConfig(FrigateBaseModel):
|
|||||||
title="Zones",
|
title="Zones",
|
||||||
description="Zones allow you to define a specific area of the frame so you can determine whether or not an object is within a particular area.",
|
description="Zones allow you to define a specific area of the frame so you can determine whether or not an object is within a particular area.",
|
||||||
)
|
)
|
||||||
enabled_in_config: Optional[bool] = Field(
|
enabled_in_config: bool | None = Field(
|
||||||
default=None,
|
default=None,
|
||||||
title="Original camera state",
|
title="Original camera state",
|
||||||
description="Keep track of original state of camera.",
|
description="Keep track of original state of camera.",
|
||||||
|
|||||||
@ -1,5 +1,3 @@
|
|||||||
from typing import Optional
|
|
||||||
|
|
||||||
from pydantic import Field, model_validator
|
from pydantic import Field, model_validator
|
||||||
|
|
||||||
from ..base import FrigateBaseModel
|
from ..base import FrigateBaseModel
|
||||||
@ -8,7 +6,7 @@ __all__ = ["DetectConfig", "StationaryConfig", "StationaryMaxFramesConfig"]
|
|||||||
|
|
||||||
|
|
||||||
class StationaryMaxFramesConfig(FrigateBaseModel):
|
class StationaryMaxFramesConfig(FrigateBaseModel):
|
||||||
default: Optional[int] = Field(
|
default: int | None = Field(
|
||||||
default=None,
|
default=None,
|
||||||
title="Default max frames",
|
title="Default max frames",
|
||||||
description="Default maximum frames to track a stationary object before stopping.",
|
description="Default maximum frames to track a stationary object before stopping.",
|
||||||
@ -22,13 +20,13 @@ class StationaryMaxFramesConfig(FrigateBaseModel):
|
|||||||
|
|
||||||
|
|
||||||
class StationaryConfig(FrigateBaseModel):
|
class StationaryConfig(FrigateBaseModel):
|
||||||
interval: Optional[int] = Field(
|
interval: int | None = Field(
|
||||||
default=None,
|
default=None,
|
||||||
title="Stationary interval",
|
title="Stationary interval",
|
||||||
description="How often (in frames) to run a detection check to confirm a stationary object.",
|
description="How often (in frames) to run a detection check to confirm a stationary object.",
|
||||||
gt=0,
|
gt=0,
|
||||||
)
|
)
|
||||||
threshold: Optional[int] = Field(
|
threshold: int | None = Field(
|
||||||
default=None,
|
default=None,
|
||||||
title="Stationary threshold",
|
title="Stationary threshold",
|
||||||
description="Number of frames with no position change required to mark an object as stationary.",
|
description="Number of frames with no position change required to mark an object as stationary.",
|
||||||
@ -52,12 +50,12 @@ class DetectConfig(FrigateBaseModel):
|
|||||||
title="Enable object detection",
|
title="Enable object detection",
|
||||||
description="Enable or disable object detection for all cameras; can be overridden per-camera.",
|
description="Enable or disable object detection for all cameras; can be overridden per-camera.",
|
||||||
)
|
)
|
||||||
height: Optional[int] = Field(
|
height: int | None = Field(
|
||||||
default=None,
|
default=None,
|
||||||
title="Detect height",
|
title="Detect height",
|
||||||
description="Height (pixels) of frames used for the detect stream; leave empty to use the native stream resolution.",
|
description="Height (pixels) of frames used for the detect stream; leave empty to use the native stream resolution.",
|
||||||
)
|
)
|
||||||
width: Optional[int] = Field(
|
width: int | None = Field(
|
||||||
default=None,
|
default=None,
|
||||||
title="Detect width",
|
title="Detect width",
|
||||||
description="Width (pixels) of frames used for the detect stream; leave empty to use the native stream resolution.",
|
description="Width (pixels) of frames used for the detect stream; leave empty to use the native stream resolution.",
|
||||||
@ -67,13 +65,13 @@ class DetectConfig(FrigateBaseModel):
|
|||||||
title="Detect FPS",
|
title="Detect FPS",
|
||||||
description="Desired frames per second to run detection on; lower values reduce CPU usage (recommended value is 5, only set higher - at most 10 - if tracking extremely fast moving objects).",
|
description="Desired frames per second to run detection on; lower values reduce CPU usage (recommended value is 5, only set higher - at most 10 - if tracking extremely fast moving objects).",
|
||||||
)
|
)
|
||||||
min_initialized: Optional[int] = Field(
|
min_initialized: int | None = Field(
|
||||||
default=None,
|
default=None,
|
||||||
title="Minimum initialization frames",
|
title="Minimum initialization frames",
|
||||||
description="Number of consecutive detection hits required before creating a tracked object. Increase to reduce false initializations. Default value is fps divided by 2.",
|
description="Number of consecutive detection hits required before creating a tracked object. Increase to reduce false initializations. Default value is fps divided by 2.",
|
||||||
ge=2,
|
ge=2,
|
||||||
)
|
)
|
||||||
max_disappeared: Optional[int] = Field(
|
max_disappeared: int | None = Field(
|
||||||
default=None,
|
default=None,
|
||||||
title="Maximum disappeared frames",
|
title="Maximum disappeared frames",
|
||||||
description="Number of frames without a detection before a tracked object is considered gone.",
|
description="Number of frames without a detection before a tracked object is considered gone.",
|
||||||
|
|||||||
@ -1,5 +1,4 @@
|
|||||||
from enum import Enum
|
from enum import Enum
|
||||||
from typing import Union
|
|
||||||
|
|
||||||
from pydantic import Field, field_validator
|
from pydantic import Field, field_validator
|
||||||
|
|
||||||
@ -33,12 +32,12 @@ DETECT_FFMPEG_OUTPUT_ARGS_DEFAULT = [
|
|||||||
|
|
||||||
|
|
||||||
class FfmpegOutputArgsConfig(FrigateBaseModel):
|
class FfmpegOutputArgsConfig(FrigateBaseModel):
|
||||||
detect: Union[str, list[str]] = Field(
|
detect: str | list[str] = Field(
|
||||||
default=DETECT_FFMPEG_OUTPUT_ARGS_DEFAULT,
|
default=DETECT_FFMPEG_OUTPUT_ARGS_DEFAULT,
|
||||||
title="Detect output arguments",
|
title="Detect output arguments",
|
||||||
description="Default output arguments for detect role streams.",
|
description="Default output arguments for detect role streams.",
|
||||||
)
|
)
|
||||||
record: Union[str, list[str]] = Field(
|
record: str | list[str] = Field(
|
||||||
default=RECORD_FFMPEG_OUTPUT_ARGS_DEFAULT,
|
default=RECORD_FFMPEG_OUTPUT_ARGS_DEFAULT,
|
||||||
title="Record output arguments",
|
title="Record output arguments",
|
||||||
description="Default output arguments for record role streams.",
|
description="Default output arguments for record role streams.",
|
||||||
@ -51,17 +50,17 @@ class FfmpegConfig(FrigateBaseModel):
|
|||||||
title="FFmpeg path",
|
title="FFmpeg path",
|
||||||
description='Path to the FFmpeg binary to use or a version alias ("7.0" or "8.0").',
|
description='Path to the FFmpeg binary to use or a version alias ("7.0" or "8.0").',
|
||||||
)
|
)
|
||||||
global_args: Union[str, list[str]] = Field(
|
global_args: str | list[str] = Field(
|
||||||
default=FFMPEG_GLOBAL_ARGS_DEFAULT,
|
default=FFMPEG_GLOBAL_ARGS_DEFAULT,
|
||||||
title="FFmpeg global arguments",
|
title="FFmpeg global arguments",
|
||||||
description="Global arguments passed to FFmpeg processes.",
|
description="Global arguments passed to FFmpeg processes.",
|
||||||
)
|
)
|
||||||
hwaccel_args: Union[str, list[str]] = Field(
|
hwaccel_args: str | list[str] = Field(
|
||||||
default="auto",
|
default="auto",
|
||||||
title="Hardware acceleration arguments",
|
title="Hardware acceleration arguments",
|
||||||
description="Hardware acceleration arguments for FFmpeg. Provider-specific presets are recommended.",
|
description="Hardware acceleration arguments for FFmpeg. Provider-specific presets are recommended.",
|
||||||
)
|
)
|
||||||
input_args: Union[str, list[str]] = Field(
|
input_args: str | list[str] = Field(
|
||||||
default=FFMPEG_INPUT_ARGS_DEFAULT,
|
default=FFMPEG_INPUT_ARGS_DEFAULT,
|
||||||
title="Input arguments",
|
title="Input arguments",
|
||||||
description="Input arguments applied to FFmpeg input streams.",
|
description="Input arguments applied to FFmpeg input streams.",
|
||||||
@ -112,17 +111,17 @@ class CameraInput(FrigateBaseModel):
|
|||||||
title="Input roles",
|
title="Input roles",
|
||||||
description="Roles for this input stream.",
|
description="Roles for this input stream.",
|
||||||
)
|
)
|
||||||
global_args: Union[str, list[str]] = Field(
|
global_args: str | list[str] = Field(
|
||||||
default_factory=list,
|
default_factory=list,
|
||||||
title="FFmpeg global arguments",
|
title="FFmpeg global arguments",
|
||||||
description="FFmpeg global arguments for this input stream.",
|
description="FFmpeg global arguments for this input stream.",
|
||||||
)
|
)
|
||||||
hwaccel_args: Union[str, list[str]] = Field(
|
hwaccel_args: str | list[str] = Field(
|
||||||
default_factory=list,
|
default_factory=list,
|
||||||
title="Hardware acceleration arguments",
|
title="Hardware acceleration arguments",
|
||||||
description="Hardware acceleration arguments for this input stream.",
|
description="Hardware acceleration arguments for this input stream.",
|
||||||
)
|
)
|
||||||
input_args: Union[str, list[str]] = Field(
|
input_args: str | list[str] = Field(
|
||||||
default_factory=list,
|
default_factory=list,
|
||||||
title="Input arguments",
|
title="Input arguments",
|
||||||
description="Input arguments specific to this stream.",
|
description="Input arguments specific to this stream.",
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
from enum import Enum
|
from enum import Enum
|
||||||
from typing import Any, Optional
|
from typing import Any
|
||||||
|
|
||||||
from pydantic import Field
|
from pydantic import Field
|
||||||
|
|
||||||
@ -26,12 +26,12 @@ class GenAIRoleEnum(str, Enum):
|
|||||||
class GenAIConfig(FrigateBaseModel):
|
class GenAIConfig(FrigateBaseModel):
|
||||||
"""Primary GenAI Config to define GenAI Provider."""
|
"""Primary GenAI Config to define GenAI Provider."""
|
||||||
|
|
||||||
api_key: Optional[EnvString] = Field(
|
api_key: EnvString | None = Field(
|
||||||
default=None,
|
default=None,
|
||||||
title="API key",
|
title="API key",
|
||||||
description="API key required by some providers (can also be set via environment variables).",
|
description="API key required by some providers (can also be set via environment variables).",
|
||||||
)
|
)
|
||||||
base_url: Optional[str] = Field(
|
base_url: str | None = Field(
|
||||||
default=None,
|
default=None,
|
||||||
title="Base URL",
|
title="Base URL",
|
||||||
description="Base URL for self-hosted or compatible providers (for example an Ollama instance).",
|
description="Base URL for self-hosted or compatible providers (for example an Ollama instance).",
|
||||||
|
|||||||
@ -1,5 +1,3 @@
|
|||||||
from typing import Dict
|
|
||||||
|
|
||||||
from pydantic import Field
|
from pydantic import Field
|
||||||
|
|
||||||
from ..base import FrigateBaseModel
|
from ..base import FrigateBaseModel
|
||||||
@ -8,7 +6,7 @@ __all__ = ["CameraLiveConfig"]
|
|||||||
|
|
||||||
|
|
||||||
class CameraLiveConfig(FrigateBaseModel):
|
class CameraLiveConfig(FrigateBaseModel):
|
||||||
streams: Dict[str, str] = Field(
|
streams: dict[str, str] = Field(
|
||||||
default_factory=list,
|
default_factory=list,
|
||||||
title="Live stream names",
|
title="Live stream names",
|
||||||
description="Mapping of configured stream names to restream/go2rtc names used for live playback.",
|
description="Mapping of configured stream names to restream/go2rtc names used for live playback.",
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
"""Mask configuration for motion and object masks."""
|
"""Mask configuration for motion and object masks."""
|
||||||
|
|
||||||
from typing import Any, Optional, Union
|
from typing import Any
|
||||||
|
|
||||||
from pydantic import Field, field_serializer
|
from pydantic import Field, field_serializer
|
||||||
|
|
||||||
@ -12,7 +12,7 @@ __all__ = ["MotionMaskConfig", "ObjectMaskConfig"]
|
|||||||
class MotionMaskConfig(FrigateBaseModel):
|
class MotionMaskConfig(FrigateBaseModel):
|
||||||
"""Configuration for a single motion mask."""
|
"""Configuration for a single motion mask."""
|
||||||
|
|
||||||
friendly_name: Optional[str] = Field(
|
friendly_name: str | None = Field(
|
||||||
default=None,
|
default=None,
|
||||||
title="Friendly name",
|
title="Friendly name",
|
||||||
description="A friendly name for this motion mask used in the Frigate UI",
|
description="A friendly name for this motion mask used in the Frigate UI",
|
||||||
@ -22,13 +22,13 @@ class MotionMaskConfig(FrigateBaseModel):
|
|||||||
title="Enabled",
|
title="Enabled",
|
||||||
description="Enable or disable this motion mask",
|
description="Enable or disable this motion mask",
|
||||||
)
|
)
|
||||||
coordinates: Union[str, list[str]] = Field(
|
coordinates: str | list[str] = Field(
|
||||||
default="",
|
default="",
|
||||||
title="Coordinates",
|
title="Coordinates",
|
||||||
description="Ordered x,y coordinates defining the motion mask polygon used to include/exclude areas.",
|
description="Ordered x,y coordinates defining the motion mask polygon used to include/exclude areas.",
|
||||||
)
|
)
|
||||||
raw_coordinates: Union[str, list[str]] = ""
|
raw_coordinates: str | list[str] = ""
|
||||||
enabled_in_config: Optional[bool] = Field(
|
enabled_in_config: bool | None = Field(
|
||||||
default=None, title="Keep track of original state of motion mask."
|
default=None, title="Keep track of original state of motion mask."
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -50,7 +50,7 @@ class MotionMaskConfig(FrigateBaseModel):
|
|||||||
class ObjectMaskConfig(FrigateBaseModel):
|
class ObjectMaskConfig(FrigateBaseModel):
|
||||||
"""Configuration for a single object mask."""
|
"""Configuration for a single object mask."""
|
||||||
|
|
||||||
friendly_name: Optional[str] = Field(
|
friendly_name: str | None = Field(
|
||||||
default=None,
|
default=None,
|
||||||
title="Friendly name",
|
title="Friendly name",
|
||||||
description="A friendly name for this object mask used in the Frigate UI",
|
description="A friendly name for this object mask used in the Frigate UI",
|
||||||
@ -60,13 +60,13 @@ class ObjectMaskConfig(FrigateBaseModel):
|
|||||||
title="Enabled",
|
title="Enabled",
|
||||||
description="Enable or disable this object mask",
|
description="Enable or disable this object mask",
|
||||||
)
|
)
|
||||||
coordinates: Union[str, list[str]] = Field(
|
coordinates: str | list[str] = Field(
|
||||||
default="",
|
default="",
|
||||||
title="Coordinates",
|
title="Coordinates",
|
||||||
description="Ordered x,y coordinates defining the object mask polygon used to include/exclude areas.",
|
description="Ordered x,y coordinates defining the object mask polygon used to include/exclude areas.",
|
||||||
)
|
)
|
||||||
raw_coordinates: Union[str, list[str]] = ""
|
raw_coordinates: str | list[str] = ""
|
||||||
enabled_in_config: Optional[bool] = Field(
|
enabled_in_config: bool | None = Field(
|
||||||
default=None, title="Keep track of original state of object mask."
|
default=None, title="Keep track of original state of object mask."
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
from typing import Any, Optional
|
from typing import Any
|
||||||
|
|
||||||
from pydantic import Field, field_serializer
|
from pydantic import Field, field_serializer
|
||||||
|
|
||||||
@ -28,7 +28,7 @@ class MotionConfig(FrigateBaseModel):
|
|||||||
ge=0.3,
|
ge=0.3,
|
||||||
le=1.0,
|
le=1.0,
|
||||||
)
|
)
|
||||||
skip_motion_threshold: Optional[float] = Field(
|
skip_motion_threshold: float | None = Field(
|
||||||
default=None,
|
default=None,
|
||||||
title="Skip motion threshold",
|
title="Skip motion threshold",
|
||||||
description="If set to a value between 0.0 and 1.0, and more than this fraction of the image changes in a single frame, the detector will return no motion boxes and immediately recalibrate. This can save CPU and reduce false positives during lightning, storms, etc., but may miss real events such as a PTZ camera auto‑tracking an object. The trade‑off is between dropping a few megabytes of recordings versus reviewing a couple short clips. Leave unset (None) to disable this feature.",
|
description="If set to a value between 0.0 and 1.0, and more than this fraction of the image changes in a single frame, the detector will return no motion boxes and immediately recalibrate. This can save CPU and reduce false positives during lightning, storms, etc., but may miss real events such as a PTZ camera auto‑tracking an object. The trade‑off is between dropping a few megabytes of recordings versus reviewing a couple short clips. Leave unset (None) to disable this feature.",
|
||||||
@ -40,7 +40,7 @@ class MotionConfig(FrigateBaseModel):
|
|||||||
title="Improve contrast",
|
title="Improve contrast",
|
||||||
description="Apply contrast improvement to frames before motion analysis to help detection.",
|
description="Apply contrast improvement to frames before motion analysis to help detection.",
|
||||||
)
|
)
|
||||||
contour_area: Optional[int] = Field(
|
contour_area: int | None = Field(
|
||||||
default=10,
|
default=10,
|
||||||
title="Contour area",
|
title="Contour area",
|
||||||
description="Minimum contour area in pixels required for a motion contour to be counted.",
|
description="Minimum contour area in pixels required for a motion contour to be counted.",
|
||||||
@ -55,12 +55,12 @@ class MotionConfig(FrigateBaseModel):
|
|||||||
title="Frame alpha",
|
title="Frame alpha",
|
||||||
description="Alpha value used when blending frames for motion preprocessing.",
|
description="Alpha value used when blending frames for motion preprocessing.",
|
||||||
)
|
)
|
||||||
frame_height: Optional[int] = Field(
|
frame_height: int | None = Field(
|
||||||
default=100,
|
default=100,
|
||||||
title="Frame height",
|
title="Frame height",
|
||||||
description="Height in pixels to scale frames to when computing motion.",
|
description="Height in pixels to scale frames to when computing motion.",
|
||||||
)
|
)
|
||||||
mask: dict[str, Optional[MotionMaskConfig]] = Field(
|
mask: dict[str, MotionMaskConfig | None] = Field(
|
||||||
default_factory=dict,
|
default_factory=dict,
|
||||||
title="Mask coordinates",
|
title="Mask coordinates",
|
||||||
description="Ordered x,y coordinates defining the motion mask polygon used to include/exclude areas.",
|
description="Ordered x,y coordinates defining the motion mask polygon used to include/exclude areas.",
|
||||||
@ -70,12 +70,12 @@ class MotionConfig(FrigateBaseModel):
|
|||||||
title="MQTT off delay",
|
title="MQTT off delay",
|
||||||
description="Seconds to wait after last motion before publishing an MQTT 'off' state.",
|
description="Seconds to wait after last motion before publishing an MQTT 'off' state.",
|
||||||
)
|
)
|
||||||
enabled_in_config: Optional[bool] = Field(
|
enabled_in_config: bool | None = Field(
|
||||||
default=None,
|
default=None,
|
||||||
title="Original motion state",
|
title="Original motion state",
|
||||||
description="Indicates whether motion detection was enabled in the original static configuration.",
|
description="Indicates whether motion detection was enabled in the original static configuration.",
|
||||||
)
|
)
|
||||||
raw_mask: dict[str, Optional[MotionMaskConfig]] = Field(
|
raw_mask: dict[str, MotionMaskConfig | None] = Field(
|
||||||
default_factory=dict, exclude=True
|
default_factory=dict, exclude=True
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@ -1,5 +1,3 @@
|
|||||||
from typing import Optional
|
|
||||||
|
|
||||||
from pydantic import Field
|
from pydantic import Field
|
||||||
|
|
||||||
from ..base import FrigateBaseModel
|
from ..base import FrigateBaseModel
|
||||||
@ -13,7 +11,7 @@ class NotificationConfig(FrigateBaseModel):
|
|||||||
title="Enable notifications",
|
title="Enable notifications",
|
||||||
description="Enable or disable notifications for all cameras; can be overridden per-camera.",
|
description="Enable or disable notifications for all cameras; can be overridden per-camera.",
|
||||||
)
|
)
|
||||||
email: Optional[str] = Field(
|
email: str | None = Field(
|
||||||
default=None,
|
default=None,
|
||||||
title="Notification email",
|
title="Notification email",
|
||||||
description="Email address used for push notifications or required by certain notification providers.",
|
description="Email address used for push notifications or required by certain notification providers.",
|
||||||
@ -24,7 +22,7 @@ class NotificationConfig(FrigateBaseModel):
|
|||||||
title="Cooldown period",
|
title="Cooldown period",
|
||||||
description="Cooldown (seconds) between notifications to avoid spamming recipients.",
|
description="Cooldown (seconds) between notifications to avoid spamming recipients.",
|
||||||
)
|
)
|
||||||
enabled_in_config: Optional[bool] = Field(
|
enabled_in_config: bool | None = Field(
|
||||||
default=None,
|
default=None,
|
||||||
title="Original notifications state",
|
title="Original notifications state",
|
||||||
description="Indicates whether notifications were enabled in the original static configuration.",
|
description="Indicates whether notifications were enabled in the original static configuration.",
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
from typing import Any, Optional, Union
|
from typing import Any
|
||||||
|
|
||||||
from pydantic import Field, PrivateAttr, field_serializer, field_validator
|
from pydantic import Field, PrivateAttr, field_serializer, field_validator
|
||||||
|
|
||||||
@ -12,12 +12,12 @@ DEFAULT_TRACKED_OBJECTS = ["person"]
|
|||||||
|
|
||||||
|
|
||||||
class FilterConfig(FrigateBaseModel):
|
class FilterConfig(FrigateBaseModel):
|
||||||
min_area: Union[int, float] = Field(
|
min_area: int | float = Field(
|
||||||
default=0,
|
default=0,
|
||||||
title="Minimum object area",
|
title="Minimum object area",
|
||||||
description="Minimum bounding box area (pixels or percentage) required for this object type. Can be pixels (int) or percentage (float between 0.000001 and 0.99).",
|
description="Minimum bounding box area (pixels or percentage) required for this object type. Can be pixels (int) or percentage (float between 0.000001 and 0.99).",
|
||||||
)
|
)
|
||||||
max_area: Union[int, float] = Field(
|
max_area: int | float = Field(
|
||||||
default=24000000,
|
default=24000000,
|
||||||
title="Maximum object area",
|
title="Maximum object area",
|
||||||
description="Maximum bounding box area (pixels or percentage) allowed for this object type. Can be pixels (int) or percentage (float between 0.000001 and 0.99).",
|
description="Maximum bounding box area (pixels or percentage) allowed for this object type. Can be pixels (int) or percentage (float between 0.000001 and 0.99).",
|
||||||
@ -42,12 +42,12 @@ class FilterConfig(FrigateBaseModel):
|
|||||||
title="Minimum confidence",
|
title="Minimum confidence",
|
||||||
description="Minimum single-frame detection confidence required for the object to be counted.",
|
description="Minimum single-frame detection confidence required for the object to be counted.",
|
||||||
)
|
)
|
||||||
mask: dict[str, Optional[ObjectMaskConfig]] = Field(
|
mask: dict[str, ObjectMaskConfig | None] = Field(
|
||||||
default_factory=dict,
|
default_factory=dict,
|
||||||
title="Filter mask",
|
title="Filter mask",
|
||||||
description="Polygon coordinates defining where this filter applies within the frame.",
|
description="Polygon coordinates defining where this filter applies within the frame.",
|
||||||
)
|
)
|
||||||
raw_mask: dict[str, Optional[ObjectMaskConfig]] = Field(
|
raw_mask: dict[str, ObjectMaskConfig | None] = Field(
|
||||||
default_factory=dict, exclude=True
|
default_factory=dict, exclude=True
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -68,7 +68,7 @@ class GenAIObjectTriggerConfig(FrigateBaseModel):
|
|||||||
title="Send on end",
|
title="Send on end",
|
||||||
description="Send a request to GenAI when the tracked object ends.",
|
description="Send a request to GenAI when the tracked object ends.",
|
||||||
)
|
)
|
||||||
after_significant_updates: Optional[int] = Field(
|
after_significant_updates: int | None = Field(
|
||||||
default=None,
|
default=None,
|
||||||
title="Early GenAI trigger",
|
title="Early GenAI trigger",
|
||||||
description="Send a request to GenAI after a specified number of significant updates for the tracked object.",
|
description="Send a request to GenAI after a specified number of significant updates for the tracked object.",
|
||||||
@ -98,12 +98,12 @@ class GenAIObjectConfig(FrigateBaseModel):
|
|||||||
description="Per-object prompts to customize GenAI outputs for specific labels.",
|
description="Per-object prompts to customize GenAI outputs for specific labels.",
|
||||||
)
|
)
|
||||||
|
|
||||||
objects: Union[str, list[str]] = Field(
|
objects: str | list[str] = Field(
|
||||||
default_factory=list,
|
default_factory=list,
|
||||||
title="GenAI objects",
|
title="GenAI objects",
|
||||||
description="List of object labels to send to GenAI by default.",
|
description="List of object labels to send to GenAI by default.",
|
||||||
)
|
)
|
||||||
required_zones: Union[str, list[str]] = Field(
|
required_zones: str | list[str] = Field(
|
||||||
default_factory=list,
|
default_factory=list,
|
||||||
title="Required zones",
|
title="Required zones",
|
||||||
description="Zones that must be entered for objects to qualify for GenAI description generation.",
|
description="Zones that must be entered for objects to qualify for GenAI description generation.",
|
||||||
@ -118,7 +118,7 @@ class GenAIObjectConfig(FrigateBaseModel):
|
|||||||
title="GenAI triggers",
|
title="GenAI triggers",
|
||||||
description="Defines when frames should be sent to GenAI (on end, after updates, etc.).",
|
description="Defines when frames should be sent to GenAI (on end, after updates, etc.).",
|
||||||
)
|
)
|
||||||
enabled_in_config: Optional[bool] = Field(
|
enabled_in_config: bool | None = Field(
|
||||||
default=None,
|
default=None,
|
||||||
title="Original GenAI state",
|
title="Original GenAI state",
|
||||||
description="Indicates whether GenAI was enabled in the original static config.",
|
description="Indicates whether GenAI was enabled in the original static config.",
|
||||||
@ -144,12 +144,12 @@ class ObjectConfig(FrigateBaseModel):
|
|||||||
title="Object filters",
|
title="Object filters",
|
||||||
description="Filters applied to detected objects to reduce false positives (area, ratio, confidence).",
|
description="Filters applied to detected objects to reduce false positives (area, ratio, confidence).",
|
||||||
)
|
)
|
||||||
mask: dict[str, Optional[ObjectMaskConfig]] = Field(
|
mask: dict[str, ObjectMaskConfig | None] = Field(
|
||||||
default_factory=dict,
|
default_factory=dict,
|
||||||
title="Object mask",
|
title="Object mask",
|
||||||
description="Mask polygon used to prevent object detection in specified areas.",
|
description="Mask polygon used to prevent object detection in specified areas.",
|
||||||
)
|
)
|
||||||
raw_mask: dict[str, Optional[ObjectMaskConfig]] = Field(
|
raw_mask: dict[str, ObjectMaskConfig | None] = Field(
|
||||||
default_factory=dict, exclude=True
|
default_factory=dict, exclude=True
|
||||||
)
|
)
|
||||||
genai: GenAIObjectConfig = Field(
|
genai: GenAIObjectConfig = Field(
|
||||||
|
|||||||
@ -1,5 +1,4 @@
|
|||||||
from enum import Enum
|
from enum import Enum
|
||||||
from typing import Optional, Union
|
|
||||||
|
|
||||||
from pydantic import Field, field_validator
|
from pydantic import Field, field_validator
|
||||||
|
|
||||||
@ -59,12 +58,12 @@ class PtzAutotrackConfig(FrigateBaseModel):
|
|||||||
title="Return timeout",
|
title="Return timeout",
|
||||||
description="Wait this many seconds after losing tracking before returning camera to preset position.",
|
description="Wait this many seconds after losing tracking before returning camera to preset position.",
|
||||||
)
|
)
|
||||||
movement_weights: Optional[Union[str, list[str]]] = Field(
|
movement_weights: str | list[str] | None = Field(
|
||||||
default_factory=list,
|
default_factory=list,
|
||||||
title="Movement weights",
|
title="Movement weights",
|
||||||
description="Calibration values automatically generated by camera calibration. Do not modify manually.",
|
description="Calibration values automatically generated by camera calibration. Do not modify manually.",
|
||||||
)
|
)
|
||||||
enabled_in_config: Optional[bool] = Field(
|
enabled_in_config: bool | None = Field(
|
||||||
default=None,
|
default=None,
|
||||||
title="Original autotrack state",
|
title="Original autotrack state",
|
||||||
description="Internal field to track whether autotracking was enabled in configuration.",
|
description="Internal field to track whether autotracking was enabled in configuration.",
|
||||||
@ -102,12 +101,12 @@ class OnvifConfig(FrigateBaseModel):
|
|||||||
title="ONVIF port",
|
title="ONVIF port",
|
||||||
description="Port number for the ONVIF service.",
|
description="Port number for the ONVIF service.",
|
||||||
)
|
)
|
||||||
user: Optional[EnvString] = Field(
|
user: EnvString | None = Field(
|
||||||
default=None,
|
default=None,
|
||||||
title="ONVIF username",
|
title="ONVIF username",
|
||||||
description="Username for ONVIF authentication; some devices require admin user for ONVIF.",
|
description="Username for ONVIF authentication; some devices require admin user for ONVIF.",
|
||||||
)
|
)
|
||||||
password: Optional[EnvString] = Field(
|
password: EnvString | None = Field(
|
||||||
default=None,
|
default=None,
|
||||||
title="ONVIF password",
|
title="ONVIF password",
|
||||||
description="Password for ONVIF authentication.",
|
description="Password for ONVIF authentication.",
|
||||||
@ -117,7 +116,7 @@ class OnvifConfig(FrigateBaseModel):
|
|||||||
title="Disable TLS verify",
|
title="Disable TLS verify",
|
||||||
description="Skip TLS verification and disable digest auth for ONVIF (unsafe; use in safe networks only).",
|
description="Skip TLS verification and disable digest auth for ONVIF (unsafe; use in safe networks only).",
|
||||||
)
|
)
|
||||||
profile: Optional[str] = Field(
|
profile: str | None = Field(
|
||||||
default=None,
|
default=None,
|
||||||
title="ONVIF profile",
|
title="ONVIF profile",
|
||||||
description="Specific ONVIF media profile to use for PTZ control, matched by token or name. If not set, the first profile with valid PTZ configuration is selected automatically.",
|
description="Specific ONVIF media profile to use for PTZ control, matched by token or name. If not set, the first profile with valid PTZ configuration is selected automatically.",
|
||||||
|
|||||||
@ -1,7 +1,5 @@
|
|||||||
"""Camera profile configuration for named config overrides."""
|
"""Camera profile configuration for named config overrides."""
|
||||||
|
|
||||||
from typing import Optional
|
|
||||||
|
|
||||||
from ..base import FrigateBaseModel
|
from ..base import FrigateBaseModel
|
||||||
from ..classification import (
|
from ..classification import (
|
||||||
CameraFaceRecognitionConfig,
|
CameraFaceRecognitionConfig,
|
||||||
@ -29,16 +27,16 @@ class CameraProfileConfig(FrigateBaseModel):
|
|||||||
explicitly-set fields are used as overrides via exclude_unset.
|
explicitly-set fields are used as overrides via exclude_unset.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
enabled: Optional[bool] = None
|
enabled: bool | None = None
|
||||||
audio: Optional[AudioConfig] = None
|
audio: AudioConfig | None = None
|
||||||
birdseye: Optional[BirdseyeCameraConfig] = None
|
birdseye: BirdseyeCameraConfig | None = None
|
||||||
detect: Optional[DetectConfig] = None
|
detect: DetectConfig | None = None
|
||||||
face_recognition: Optional[CameraFaceRecognitionConfig] = None
|
face_recognition: CameraFaceRecognitionConfig | None = None
|
||||||
lpr: Optional[CameraLicensePlateRecognitionConfig] = None
|
lpr: CameraLicensePlateRecognitionConfig | None = None
|
||||||
motion: Optional[MotionConfig] = None
|
motion: MotionConfig | None = None
|
||||||
notifications: Optional[NotificationConfig] = None
|
notifications: NotificationConfig | None = None
|
||||||
objects: Optional[ObjectConfig] = None
|
objects: ObjectConfig | None = None
|
||||||
record: Optional[RecordConfig] = None
|
record: RecordConfig | None = None
|
||||||
review: Optional[ReviewConfig] = None
|
review: ReviewConfig | None = None
|
||||||
snapshots: Optional[SnapshotsConfig] = None
|
snapshots: SnapshotsConfig | None = None
|
||||||
zones: Optional[dict[str, ZoneConfig]] = None
|
zones: dict[str, ZoneConfig] | None = None
|
||||||
|
|||||||
@ -1,5 +1,4 @@
|
|||||||
from enum import Enum
|
from enum import Enum
|
||||||
from typing import Optional, Union
|
|
||||||
|
|
||||||
from pydantic import Field
|
from pydantic import Field
|
||||||
|
|
||||||
@ -94,7 +93,7 @@ class ChaptersEnum(str, Enum):
|
|||||||
|
|
||||||
|
|
||||||
class RecordExportConfig(FrigateBaseModel):
|
class RecordExportConfig(FrigateBaseModel):
|
||||||
hwaccel_args: Union[str, list[str]] = Field(
|
hwaccel_args: str | list[str] = Field(
|
||||||
default="auto",
|
default="auto",
|
||||||
title="Export hwaccel args",
|
title="Export hwaccel args",
|
||||||
description="Hardware acceleration args to use for export/transcode operations.",
|
description="Hardware acceleration args to use for export/transcode operations.",
|
||||||
@ -152,7 +151,7 @@ class RecordConfig(FrigateBaseModel):
|
|||||||
title="Preview config",
|
title="Preview config",
|
||||||
description="Settings controlling the quality of recording previews shown in the UI.",
|
description="Settings controlling the quality of recording previews shown in the UI.",
|
||||||
)
|
)
|
||||||
enabled_in_config: Optional[bool] = Field(
|
enabled_in_config: bool | None = Field(
|
||||||
default=None,
|
default=None,
|
||||||
title="Original recording state",
|
title="Original recording state",
|
||||||
description="Indicates whether recording was enabled in the original static configuration.",
|
description="Indicates whether recording was enabled in the original static configuration.",
|
||||||
|
|||||||
@ -1,5 +1,4 @@
|
|||||||
from enum import Enum
|
from enum import Enum
|
||||||
from typing import Optional, Union
|
|
||||||
|
|
||||||
from pydantic import Field, field_validator
|
from pydantic import Field, field_validator
|
||||||
|
|
||||||
@ -32,13 +31,13 @@ class AlertsConfig(FrigateBaseModel):
|
|||||||
title="Alert labels",
|
title="Alert labels",
|
||||||
description="List of object labels that qualify as alerts (for example: car, person).",
|
description="List of object labels that qualify as alerts (for example: car, person).",
|
||||||
)
|
)
|
||||||
required_zones: Union[str, list[str]] = Field(
|
required_zones: str | list[str] = Field(
|
||||||
default_factory=list,
|
default_factory=list,
|
||||||
title="Required zones",
|
title="Required zones",
|
||||||
description="Zones that an object must enter to be considered an alert; leave empty to allow any zone.",
|
description="Zones that an object must enter to be considered an alert; leave empty to allow any zone.",
|
||||||
)
|
)
|
||||||
|
|
||||||
enabled_in_config: Optional[bool] = Field(
|
enabled_in_config: bool | None = Field(
|
||||||
default=None,
|
default=None,
|
||||||
title="Original alerts state",
|
title="Original alerts state",
|
||||||
description="Tracks whether alerts were originally enabled in the static configuration.",
|
description="Tracks whether alerts were originally enabled in the static configuration.",
|
||||||
@ -67,12 +66,12 @@ class DetectionsConfig(FrigateBaseModel):
|
|||||||
description="Enable or disable detection events for all cameras; can be overridden per-camera.",
|
description="Enable or disable detection events for all cameras; can be overridden per-camera.",
|
||||||
)
|
)
|
||||||
|
|
||||||
labels: Optional[list[str]] = Field(
|
labels: list[str] | None = Field(
|
||||||
default=None,
|
default=None,
|
||||||
title="Detection labels",
|
title="Detection labels",
|
||||||
description="List of object labels that qualify as detection events.",
|
description="List of object labels that qualify as detection events.",
|
||||||
)
|
)
|
||||||
required_zones: Union[str, list[str]] = Field(
|
required_zones: str | list[str] = Field(
|
||||||
default_factory=list,
|
default_factory=list,
|
||||||
title="Required zones",
|
title="Required zones",
|
||||||
description="Zones that an object must enter to be considered a detection; leave empty to allow any zone.",
|
description="Zones that an object must enter to be considered a detection; leave empty to allow any zone.",
|
||||||
@ -83,7 +82,7 @@ class DetectionsConfig(FrigateBaseModel):
|
|||||||
description="Seconds to wait after no detection-causing activity before cutting off a detection.",
|
description="Seconds to wait after no detection-causing activity before cutting off a detection.",
|
||||||
)
|
)
|
||||||
|
|
||||||
enabled_in_config: Optional[bool] = Field(
|
enabled_in_config: bool | None = Field(
|
||||||
default=None,
|
default=None,
|
||||||
title="Original detections state",
|
title="Original detections state",
|
||||||
description="Tracks whether detections were originally enabled in the static configuration.",
|
description="Tracks whether detections were originally enabled in the static configuration.",
|
||||||
@ -129,7 +128,7 @@ class GenAIReviewConfig(FrigateBaseModel):
|
|||||||
title="Save thumbnails",
|
title="Save thumbnails",
|
||||||
description="Save thumbnails that are sent to the GenAI provider for debugging and review.",
|
description="Save thumbnails that are sent to the GenAI provider for debugging and review.",
|
||||||
)
|
)
|
||||||
enabled_in_config: Optional[bool] = Field(
|
enabled_in_config: bool | None = Field(
|
||||||
default=None,
|
default=None,
|
||||||
title="Original GenAI state",
|
title="Original GenAI state",
|
||||||
description="Tracks whether GenAI review was originally enabled in the static configuration.",
|
description="Tracks whether GenAI review was originally enabled in the static configuration.",
|
||||||
|
|||||||
@ -1,5 +1,3 @@
|
|||||||
from typing import Optional
|
|
||||||
|
|
||||||
from pydantic import Field
|
from pydantic import Field
|
||||||
|
|
||||||
from ..base import FrigateBaseModel
|
from ..base import FrigateBaseModel
|
||||||
@ -46,7 +44,7 @@ class SnapshotsConfig(FrigateBaseModel):
|
|||||||
title="Required zones",
|
title="Required zones",
|
||||||
description="Zones an object must enter for a snapshot to be saved.",
|
description="Zones an object must enter for a snapshot to be saved.",
|
||||||
)
|
)
|
||||||
height: Optional[int] = Field(
|
height: int | None = Field(
|
||||||
default=None,
|
default=None,
|
||||||
title="Snapshot height",
|
title="Snapshot height",
|
||||||
description="Height (pixels) to resize snapshots from API to; leave empty to preserve original size.",
|
description="Height (pixels) to resize snapshots from API to; leave empty to preserve original size.",
|
||||||
|
|||||||
@ -1,5 +1,4 @@
|
|||||||
from enum import Enum
|
from enum import Enum
|
||||||
from typing import Optional
|
|
||||||
|
|
||||||
from pydantic import Field
|
from pydantic import Field
|
||||||
|
|
||||||
@ -76,7 +75,7 @@ class TimestampStyleConfig(FrigateBaseModel):
|
|||||||
title="Timestamp thickness",
|
title="Timestamp thickness",
|
||||||
description="Line thickness of the timestamp text.",
|
description="Line thickness of the timestamp text.",
|
||||||
)
|
)
|
||||||
effect: Optional[TimestampEffectEnum] = Field(
|
effect: TimestampEffectEnum | None = Field(
|
||||||
default=None,
|
default=None,
|
||||||
title="Timestamp effect",
|
title="Timestamp effect",
|
||||||
description="Visual effect for the timestamp text (none, solid, shadow).",
|
description="Visual effect for the timestamp text (none, solid, shadow).",
|
||||||
|
|||||||
@ -1,6 +1,5 @@
|
|||||||
# this uses the base model because the color is an extra attribute
|
# this uses the base model because the color is an extra attribute
|
||||||
import logging
|
import logging
|
||||||
from typing import Optional, Union
|
|
||||||
|
|
||||||
import numpy as np
|
import numpy as np
|
||||||
from pydantic import BaseModel, Field, PrivateAttr, field_validator, model_validator
|
from pydantic import BaseModel, Field, PrivateAttr, field_validator, model_validator
|
||||||
@ -13,7 +12,7 @@ logger = logging.getLogger(__name__)
|
|||||||
|
|
||||||
|
|
||||||
class ZoneConfig(BaseModel):
|
class ZoneConfig(BaseModel):
|
||||||
friendly_name: Optional[str] = Field(
|
friendly_name: str | None = Field(
|
||||||
None,
|
None,
|
||||||
title="Zone name",
|
title="Zone name",
|
||||||
description="A user-friendly name for the zone, displayed in the Frigate UI. If not set, a formatted version of the zone name will be used.",
|
description="A user-friendly name for the zone, displayed in the Frigate UI. If not set, a formatted version of the zone name will be used.",
|
||||||
@ -23,7 +22,7 @@ class ZoneConfig(BaseModel):
|
|||||||
title="Enabled",
|
title="Enabled",
|
||||||
description="Enable or disable this zone. Disabled zones are ignored at runtime.",
|
description="Enable or disable this zone. Disabled zones are ignored at runtime.",
|
||||||
)
|
)
|
||||||
enabled_in_config: Optional[bool] = Field(
|
enabled_in_config: bool | None = Field(
|
||||||
default=None, title="Keep track of original state of zone."
|
default=None, title="Keep track of original state of zone."
|
||||||
)
|
)
|
||||||
filters: dict[str, FilterConfig] = Field(
|
filters: dict[str, FilterConfig] = Field(
|
||||||
@ -31,11 +30,11 @@ class ZoneConfig(BaseModel):
|
|||||||
title="Zone filters",
|
title="Zone filters",
|
||||||
description="Filters to apply to objects within this zone. Used to reduce false positives or restrict which objects are considered present in the zone.",
|
description="Filters to apply to objects within this zone. Used to reduce false positives or restrict which objects are considered present in the zone.",
|
||||||
)
|
)
|
||||||
coordinates: Union[str, list[str]] = Field(
|
coordinates: str | list[str] = Field(
|
||||||
title="Coordinates",
|
title="Coordinates",
|
||||||
description="Polygon coordinates that define the zone area. Can be a comma-separated string or a list of coordinate strings. Coordinates should be relative (0-1) or absolute (legacy).",
|
description="Polygon coordinates that define the zone area. Can be a comma-separated string or a list of coordinate strings. Coordinates should be relative (0-1) or absolute (legacy).",
|
||||||
)
|
)
|
||||||
distances: Optional[Union[str, list[str]]] = Field(
|
distances: str | list[str] | None = Field(
|
||||||
default_factory=list,
|
default_factory=list,
|
||||||
title="Real-world distances",
|
title="Real-world distances",
|
||||||
description="Optional real-world distances for each side of the zone quadrilateral, used for speed or distance calculations. Must have exactly 4 values if set.",
|
description="Optional real-world distances for each side of the zone quadrilateral, used for speed or distance calculations. Must have exactly 4 values if set.",
|
||||||
@ -52,18 +51,18 @@ class ZoneConfig(BaseModel):
|
|||||||
title="Loitering seconds",
|
title="Loitering seconds",
|
||||||
description="Number of seconds an object must remain in the zone to be considered as loitering. Set to 0 to disable loitering detection.",
|
description="Number of seconds an object must remain in the zone to be considered as loitering. Set to 0 to disable loitering detection.",
|
||||||
)
|
)
|
||||||
speed_threshold: Optional[float] = Field(
|
speed_threshold: float | None = Field(
|
||||||
default=None,
|
default=None,
|
||||||
ge=0.1,
|
ge=0.1,
|
||||||
title="Minimum speed",
|
title="Minimum speed",
|
||||||
description="Minimum speed (in real-world units if distances are set) required for an object to be considered present in the zone. Used for speed-based zone triggers.",
|
description="Minimum speed (in real-world units if distances are set) required for an object to be considered present in the zone. Used for speed-based zone triggers.",
|
||||||
)
|
)
|
||||||
objects: Union[str, list[str]] = Field(
|
objects: str | list[str] = Field(
|
||||||
default_factory=list,
|
default_factory=list,
|
||||||
title="Trigger objects",
|
title="Trigger objects",
|
||||||
description="List of object types (from labelmap) that can trigger this zone. Can be a string or a list of strings. If empty, all objects are considered.",
|
description="List of object types (from labelmap) that can trigger this zone. Can be a string or a list of strings. If empty, all objects are considered.",
|
||||||
)
|
)
|
||||||
_color: Optional[tuple[int, int, int]] = PrivateAttr()
|
_color: tuple[int, int, int] | None = PrivateAttr()
|
||||||
_contour: np.ndarray = PrivateAttr()
|
_contour: np.ndarray = PrivateAttr()
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@ -147,7 +146,7 @@ class ZoneConfig(BaseModel):
|
|||||||
except ValueError:
|
except ValueError:
|
||||||
raise ValueError(
|
raise ValueError(
|
||||||
f"Invalid coordinates found in configuration file. Coordinates must be relative (between 0-1): {coordinates}"
|
f"Invalid coordinates found in configuration file. Coordinates must be relative (between 0-1): {coordinates}"
|
||||||
)
|
) from None
|
||||||
|
|
||||||
if explicit:
|
if explicit:
|
||||||
self.coordinates = ",".join(
|
self.coordinates = ",".join(
|
||||||
@ -176,7 +175,7 @@ class ZoneConfig(BaseModel):
|
|||||||
except ValueError:
|
except ValueError:
|
||||||
raise ValueError(
|
raise ValueError(
|
||||||
f"Invalid coordinates found in configuration file. Coordinates must be relative (between 0-1): {coordinates}"
|
f"Invalid coordinates found in configuration file. Coordinates must be relative (between 0-1): {coordinates}"
|
||||||
)
|
) from None
|
||||||
|
|
||||||
if explicit:
|
if explicit:
|
||||||
self.coordinates = ",".join(
|
self.coordinates = ",".join(
|
||||||
|
|||||||
@ -1,5 +1,3 @@
|
|||||||
from typing import Union
|
|
||||||
|
|
||||||
from pydantic import Field, field_validator
|
from pydantic import Field, field_validator
|
||||||
|
|
||||||
from .base import FrigateBaseModel
|
from .base import FrigateBaseModel
|
||||||
@ -8,7 +6,7 @@ __all__ = ["CameraGroupConfig"]
|
|||||||
|
|
||||||
|
|
||||||
class CameraGroupConfig(FrigateBaseModel):
|
class CameraGroupConfig(FrigateBaseModel):
|
||||||
cameras: Union[str, list[str]] = Field(
|
cameras: str | list[str] = Field(
|
||||||
default_factory=list,
|
default_factory=list,
|
||||||
title="Camera list",
|
title="Camera list",
|
||||||
description="Array of camera names included in this group.",
|
description="Array of camera names included in this group.",
|
||||||
|
|||||||
@ -1,5 +1,4 @@
|
|||||||
from enum import Enum
|
from enum import Enum
|
||||||
from typing import Dict, List, Optional, Union
|
|
||||||
|
|
||||||
from pydantic import ConfigDict, Field, field_validator
|
from pydantic import ConfigDict, Field, field_validator
|
||||||
|
|
||||||
@ -68,7 +67,7 @@ class AudioTranscriptionConfig(FrigateBaseModel):
|
|||||||
title="Model size",
|
title="Model size",
|
||||||
description="Model size to use for offline audio event transcription.",
|
description="Model size to use for offline audio event transcription.",
|
||||||
)
|
)
|
||||||
live_enabled: Optional[bool] = Field(
|
live_enabled: bool | None = Field(
|
||||||
default=False,
|
default=False,
|
||||||
title="Live transcription",
|
title="Live transcription",
|
||||||
description="Enable streaming live transcription for audio as it is received.",
|
description="Enable streaming live transcription for audio as it is received.",
|
||||||
@ -98,7 +97,7 @@ class CustomClassificationStateCameraConfig(FrigateBaseModel):
|
|||||||
|
|
||||||
|
|
||||||
class CustomClassificationStateConfig(FrigateBaseModel):
|
class CustomClassificationStateConfig(FrigateBaseModel):
|
||||||
cameras: Dict[str, CustomClassificationStateCameraConfig] = Field(
|
cameras: dict[str, CustomClassificationStateCameraConfig] = Field(
|
||||||
title="Classification cameras",
|
title="Classification cameras",
|
||||||
description="Per-camera crop and settings for running state classification.",
|
description="Per-camera crop and settings for running state classification.",
|
||||||
)
|
)
|
||||||
@ -160,7 +159,7 @@ class ClassificationConfig(FrigateBaseModel):
|
|||||||
title="Bird classification config",
|
title="Bird classification config",
|
||||||
description="Settings specific to bird classification models.",
|
description="Settings specific to bird classification models.",
|
||||||
)
|
)
|
||||||
custom: Dict[str, CustomClassificationConfig] = Field(
|
custom: dict[str, CustomClassificationConfig] = Field(
|
||||||
default={},
|
default={},
|
||||||
title="Custom Classification Models",
|
title="Custom Classification Models",
|
||||||
description="Configuration for custom classification models used for objects or state detection.",
|
description="Configuration for custom classification models used for objects or state detection.",
|
||||||
@ -173,12 +172,12 @@ class SemanticSearchConfig(FrigateBaseModel):
|
|||||||
title="Enable semantic search",
|
title="Enable semantic search",
|
||||||
description="Enable or disable the semantic search feature.",
|
description="Enable or disable the semantic search feature.",
|
||||||
)
|
)
|
||||||
reindex: Optional[bool] = Field(
|
reindex: bool | None = Field(
|
||||||
default=False,
|
default=False,
|
||||||
title="Reindex on startup",
|
title="Reindex on startup",
|
||||||
description="Trigger a full reindex of historical tracked objects into the embeddings database.",
|
description="Trigger a full reindex of historical tracked objects into the embeddings database.",
|
||||||
)
|
)
|
||||||
model: Optional[Union[SemanticSearchModelEnum, str]] = Field(
|
model: SemanticSearchModelEnum | str | None = Field(
|
||||||
default=SemanticSearchModelEnum.jinav1,
|
default=SemanticSearchModelEnum.jinav1,
|
||||||
title="Semantic search model or GenAI provider name",
|
title="Semantic search model or GenAI provider name",
|
||||||
description="The embeddings model to use for semantic search (for example 'jinav1'), or the name of a GenAI provider with the embeddings role.",
|
description="The embeddings model to use for semantic search (for example 'jinav1'), or the name of a GenAI provider with the embeddings role.",
|
||||||
@ -199,7 +198,7 @@ class SemanticSearchConfig(FrigateBaseModel):
|
|||||||
title="Model size",
|
title="Model size",
|
||||||
description="Select model size; 'small' runs on CPU and 'large' typically requires GPU.",
|
description="Select model size; 'small' runs on CPU and 'large' typically requires GPU.",
|
||||||
)
|
)
|
||||||
device: Optional[str] = Field(
|
device: str | None = Field(
|
||||||
default=None,
|
default=None,
|
||||||
title="Device",
|
title="Device",
|
||||||
description="This is an override, to target a specific device. See https://onnxruntime.ai/docs/execution-providers/ for more information",
|
description="This is an override, to target a specific device. See https://onnxruntime.ai/docs/execution-providers/ for more information",
|
||||||
@ -207,7 +206,7 @@ class SemanticSearchConfig(FrigateBaseModel):
|
|||||||
|
|
||||||
|
|
||||||
class TriggerConfig(FrigateBaseModel):
|
class TriggerConfig(FrigateBaseModel):
|
||||||
friendly_name: Optional[str] = Field(
|
friendly_name: str | None = Field(
|
||||||
None,
|
None,
|
||||||
title="Friendly name",
|
title="Friendly name",
|
||||||
description="Optional friendly name displayed in the UI for this trigger.",
|
description="Optional friendly name displayed in the UI for this trigger.",
|
||||||
@ -233,7 +232,7 @@ class TriggerConfig(FrigateBaseModel):
|
|||||||
gt=0.0,
|
gt=0.0,
|
||||||
le=1.0,
|
le=1.0,
|
||||||
)
|
)
|
||||||
actions: List[TriggerAction] = Field(
|
actions: list[TriggerAction] = Field(
|
||||||
default=[],
|
default=[],
|
||||||
title="Trigger actions",
|
title="Trigger actions",
|
||||||
description="List of actions to execute when trigger matches (notification, sub_label, attribute).",
|
description="List of actions to execute when trigger matches (notification, sub_label, attribute).",
|
||||||
@ -243,7 +242,7 @@ class TriggerConfig(FrigateBaseModel):
|
|||||||
|
|
||||||
|
|
||||||
class CameraSemanticSearchConfig(FrigateBaseModel):
|
class CameraSemanticSearchConfig(FrigateBaseModel):
|
||||||
triggers: Dict[str, TriggerConfig] = Field(
|
triggers: dict[str, TriggerConfig] = Field(
|
||||||
default={},
|
default={},
|
||||||
title="Triggers",
|
title="Triggers",
|
||||||
description="Actions and matching criteria for camera-specific semantic search triggers.",
|
description="Actions and matching criteria for camera-specific semantic search triggers.",
|
||||||
@ -307,7 +306,7 @@ class FaceRecognitionConfig(FrigateBaseModel):
|
|||||||
title="Blur confidence filter",
|
title="Blur confidence filter",
|
||||||
description="Adjust confidence scores based on image blur to reduce false positives for poor quality faces.",
|
description="Adjust confidence scores based on image blur to reduce false positives for poor quality faces.",
|
||||||
)
|
)
|
||||||
device: Optional[str] = Field(
|
device: str | None = Field(
|
||||||
default=None,
|
default=None,
|
||||||
title="Device",
|
title="Device",
|
||||||
description="This is an override, to target a specific device. See https://onnxruntime.ai/docs/execution-providers/ for more information",
|
description="This is an override, to target a specific device. See https://onnxruntime.ai/docs/execution-providers/ for more information",
|
||||||
@ -369,7 +368,7 @@ class LicensePlateRecognitionConfig(FrigateBaseModel):
|
|||||||
title="Min plate length",
|
title="Min plate length",
|
||||||
description="Minimum number of characters a recognized plate must contain to be considered valid.",
|
description="Minimum number of characters a recognized plate must contain to be considered valid.",
|
||||||
)
|
)
|
||||||
format: Optional[str] = Field(
|
format: str | None = Field(
|
||||||
default=None,
|
default=None,
|
||||||
title="Plate format regex",
|
title="Plate format regex",
|
||||||
description="Optional regex to validate recognized plate strings against an expected format.",
|
description="Optional regex to validate recognized plate strings against an expected format.",
|
||||||
@ -380,7 +379,7 @@ class LicensePlateRecognitionConfig(FrigateBaseModel):
|
|||||||
description="Number of character mismatches allowed when comparing detected plates to known plates.",
|
description="Number of character mismatches allowed when comparing detected plates to known plates.",
|
||||||
ge=0,
|
ge=0,
|
||||||
)
|
)
|
||||||
known_plates: Optional[Dict[str, List[str]]] = Field(
|
known_plates: dict[str, list[str]] | None = Field(
|
||||||
default={},
|
default={},
|
||||||
title="Known plates",
|
title="Known plates",
|
||||||
description="List of plates or regexes to specially track or alert on.",
|
description="List of plates or regexes to specially track or alert on.",
|
||||||
@ -397,12 +396,12 @@ class LicensePlateRecognitionConfig(FrigateBaseModel):
|
|||||||
title="Save debug plates",
|
title="Save debug plates",
|
||||||
description="Save plate crop images for debugging LPR performance.",
|
description="Save plate crop images for debugging LPR performance.",
|
||||||
)
|
)
|
||||||
device: Optional[str] = Field(
|
device: str | None = Field(
|
||||||
default=None,
|
default=None,
|
||||||
title="Device",
|
title="Device",
|
||||||
description="This is an override, to target a specific device. See https://onnxruntime.ai/docs/execution-providers/ for more information",
|
description="This is an override, to target a specific device. See https://onnxruntime.ai/docs/execution-providers/ for more information",
|
||||||
)
|
)
|
||||||
replace_rules: List[ReplaceRule] = Field(
|
replace_rules: list[ReplaceRule] = Field(
|
||||||
default_factory=list,
|
default_factory=list,
|
||||||
title="Replacement rules",
|
title="Replacement rules",
|
||||||
description="Regex replacement rules used to normalize detected plate strings before matching.",
|
description="Regex replacement rules used to normalize detected plate strings before matching.",
|
||||||
@ -443,10 +442,10 @@ class CameraAudioTranscriptionConfig(FrigateBaseModel):
|
|||||||
title="Enable transcription",
|
title="Enable transcription",
|
||||||
description="Enable or disable manually triggered audio event transcription.",
|
description="Enable or disable manually triggered audio event transcription.",
|
||||||
)
|
)
|
||||||
enabled_in_config: Optional[bool] = Field(
|
enabled_in_config: bool | None = Field(
|
||||||
default=None, title="Original transcription state"
|
default=None, title="Original transcription state"
|
||||||
)
|
)
|
||||||
live_enabled: Optional[bool] = Field(
|
live_enabled: bool | None = Field(
|
||||||
default=False,
|
default=False,
|
||||||
title="Live transcription",
|
title="Live transcription",
|
||||||
description="Enable streaming live transcription for audio as it is received.",
|
description="Enable streaming live transcription for audio as it is received.",
|
||||||
|
|||||||
@ -4,7 +4,7 @@ import io
|
|||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
from typing import Any, Dict, Optional
|
from typing import Any, Self
|
||||||
|
|
||||||
import numpy as np
|
import numpy as np
|
||||||
from pydantic import (
|
from pydantic import (
|
||||||
@ -17,7 +17,6 @@ from pydantic import (
|
|||||||
model_validator,
|
model_validator,
|
||||||
)
|
)
|
||||||
from ruamel.yaml import YAML
|
from ruamel.yaml import YAML
|
||||||
from typing_extensions import Self
|
|
||||||
|
|
||||||
from frigate.const import REGEX_JSON
|
from frigate.const import REGEX_JSON
|
||||||
from frigate.detectors import DetectorConfig, ModelConfig
|
from frigate.detectors import DetectorConfig, ModelConfig
|
||||||
@ -174,7 +173,7 @@ class RuntimeMotionConfig(MotionConfig):
|
|||||||
class RuntimeFilterConfig(FilterConfig):
|
class RuntimeFilterConfig(FilterConfig):
|
||||||
"""Runtime version of FilterConfig with rasterized masks."""
|
"""Runtime version of FilterConfig with rasterized masks."""
|
||||||
|
|
||||||
rasterized_mask: Optional[np.ndarray] = Field(default=None, exclude=True)
|
rasterized_mask: np.ndarray | None = Field(default=None, exclude=True)
|
||||||
|
|
||||||
def __init__(self, **config):
|
def __init__(self, **config):
|
||||||
frame_shape = config.get("frame_shape", (1, 1))
|
frame_shape = config.get("frame_shape", (1, 1))
|
||||||
@ -293,7 +292,7 @@ def verify_recording_segments_setup_with_reasonable_time(
|
|||||||
raise ValueError(
|
raise ValueError(
|
||||||
f"Camera {camera_config.name} has no segment_time in \
|
f"Camera {camera_config.name} has no segment_time in \
|
||||||
recording output args, segment args are required for record."
|
recording output args, segment args are required for record."
|
||||||
)
|
) from None
|
||||||
|
|
||||||
if int(record_args[seg_arg_index + 1]) > 60:
|
if int(record_args[seg_arg_index + 1]) > 60:
|
||||||
raise ValueError(
|
raise ValueError(
|
||||||
@ -420,7 +419,7 @@ def verify_lpr_and_face(
|
|||||||
|
|
||||||
|
|
||||||
class FrigateConfig(FrigateBaseModel):
|
class FrigateConfig(FrigateBaseModel):
|
||||||
version: Optional[str] = Field(
|
version: str | None = Field(
|
||||||
default=None,
|
default=None,
|
||||||
title="Current config version",
|
title="Current config version",
|
||||||
description="Numeric or string version of the active configuration to help detect migrations or format changes.",
|
description="Numeric or string version of the active configuration to help detect migrations or format changes.",
|
||||||
@ -496,7 +495,7 @@ class FrigateConfig(FrigateBaseModel):
|
|||||||
)
|
)
|
||||||
|
|
||||||
# Detector config
|
# Detector config
|
||||||
detectors: Dict[str, BaseDetectorConfig] = Field(
|
detectors: dict[str, BaseDetectorConfig] = Field(
|
||||||
default=DEFAULT_DETECTORS,
|
default=DEFAULT_DETECTORS,
|
||||||
title="Detector hardware",
|
title="Detector hardware",
|
||||||
description="Configuration for object detectors (CPU, GPU, ONNX backends) and any detector-specific model settings.",
|
description="Configuration for object detectors (CPU, GPU, ONNX backends) and any detector-specific model settings.",
|
||||||
@ -508,14 +507,14 @@ class FrigateConfig(FrigateBaseModel):
|
|||||||
)
|
)
|
||||||
|
|
||||||
# GenAI config (named provider configs: name -> GenAIConfig)
|
# GenAI config (named provider configs: name -> GenAIConfig)
|
||||||
genai: Dict[str, GenAIConfig] = Field(
|
genai: dict[str, GenAIConfig] = Field(
|
||||||
default_factory=dict,
|
default_factory=dict,
|
||||||
title="Generative AI configuration",
|
title="Generative AI configuration",
|
||||||
description="Settings for integrated generative AI providers used to generate object descriptions and review summaries.",
|
description="Settings for integrated generative AI providers used to generate object descriptions and review summaries.",
|
||||||
)
|
)
|
||||||
|
|
||||||
# Camera config
|
# Camera config
|
||||||
cameras: Dict[str, CameraConfig] = Field(title="Cameras", description="Cameras")
|
cameras: dict[str, CameraConfig] = Field(title="Cameras", description="Cameras")
|
||||||
audio: AudioConfig = Field(
|
audio: AudioConfig = Field(
|
||||||
default_factory=AudioConfig,
|
default_factory=AudioConfig,
|
||||||
title="Audio detection",
|
title="Audio detection",
|
||||||
@ -541,7 +540,7 @@ class FrigateConfig(FrigateBaseModel):
|
|||||||
title="Live playback",
|
title="Live playback",
|
||||||
description="Settings to control the jsmpeg live stream resolution and quality. This does not affect restreamed cameras that use go2rtc for live view.",
|
description="Settings to control the jsmpeg live stream resolution and quality. This does not affect restreamed cameras that use go2rtc for live view.",
|
||||||
)
|
)
|
||||||
motion: Optional[MotionConfig] = Field(
|
motion: MotionConfig | None = Field(
|
||||||
default=None,
|
default=None,
|
||||||
title="Motion detection",
|
title="Motion detection",
|
||||||
description="Default motion detection settings applied to cameras unless overridden per-camera.",
|
description="Default motion detection settings applied to cameras unless overridden per-camera.",
|
||||||
@ -599,19 +598,19 @@ class FrigateConfig(FrigateBaseModel):
|
|||||||
description="License plate recognition settings including detection thresholds, formatting, and known plates.",
|
description="License plate recognition settings including detection thresholds, formatting, and known plates.",
|
||||||
)
|
)
|
||||||
|
|
||||||
camera_groups: Dict[str, CameraGroupConfig] = Field(
|
camera_groups: dict[str, CameraGroupConfig] = Field(
|
||||||
default_factory=dict,
|
default_factory=dict,
|
||||||
title="Camera groups",
|
title="Camera groups",
|
||||||
description="Configuration for named camera groups used to organize cameras in the UI.",
|
description="Configuration for named camera groups used to organize cameras in the UI.",
|
||||||
)
|
)
|
||||||
|
|
||||||
profiles: Dict[str, ProfileDefinitionConfig] = Field(
|
profiles: dict[str, ProfileDefinitionConfig] = Field(
|
||||||
default_factory=dict,
|
default_factory=dict,
|
||||||
title="Profiles",
|
title="Profiles",
|
||||||
description="Named profile definitions with friendly names. Camera profiles must reference names defined here.",
|
description="Named profile definitions with friendly names. Camera profiles must reference names defined here.",
|
||||||
)
|
)
|
||||||
|
|
||||||
active_profile: Optional[str] = Field(
|
active_profile: str | None = Field(
|
||||||
default=None,
|
default=None,
|
||||||
title="Active profile",
|
title="Active profile",
|
||||||
description="Currently active profile name. Runtime-only, not persisted in YAML.",
|
description="Currently active profile name. Runtime-only, not persisted in YAML.",
|
||||||
@ -1054,7 +1053,7 @@ class FrigateConfig(FrigateBaseModel):
|
|||||||
|
|
||||||
@field_validator("cameras")
|
@field_validator("cameras")
|
||||||
@classmethod
|
@classmethod
|
||||||
def ensure_zones_and_cameras_have_different_names(cls, v: Dict[str, CameraConfig]):
|
def ensure_zones_and_cameras_have_different_names(cls, v: dict[str, CameraConfig]):
|
||||||
zones = [zone for camera in v.values() for zone in camera.zones.keys()]
|
zones = [zone for camera in v.values() for zone in camera.zones.keys()]
|
||||||
for zone in zones:
|
for zone in zones:
|
||||||
if zone in v.keys():
|
if zone in v.keys():
|
||||||
@ -1136,7 +1135,7 @@ class FrigateConfig(FrigateBaseModel):
|
|||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def parse_object(
|
def parse_object(
|
||||||
cls, obj: Any, *, plus_api: Optional[PlusApi] = None, install: bool = False
|
cls, obj: Any, *, plus_api: PlusApi | None = None, install: bool = False
|
||||||
):
|
):
|
||||||
return cls.model_validate(
|
return cls.model_validate(
|
||||||
obj, context={"plus_api": plus_api, "install": install}
|
obj, context={"plus_api": plus_api, "install": install}
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
|
from typing import Self
|
||||||
|
|
||||||
from pydantic import Field, ValidationInfo, model_validator
|
from pydantic import Field, ValidationInfo, model_validator
|
||||||
from typing_extensions import Self
|
|
||||||
|
|
||||||
from frigate.log import LogLevel, apply_log_levels
|
from frigate.log import LogLevel, apply_log_levels
|
||||||
|
|
||||||
|
|||||||
@ -1,7 +1,6 @@
|
|||||||
from typing import Optional
|
from typing import Self
|
||||||
|
|
||||||
from pydantic import Field, ValidationInfo, model_validator
|
from pydantic import Field, ValidationInfo, model_validator
|
||||||
from typing_extensions import Self
|
|
||||||
|
|
||||||
from frigate.const import FREQUENCY_STATS_POINTS
|
from frigate.const import FREQUENCY_STATS_POINTS
|
||||||
|
|
||||||
@ -43,33 +42,33 @@ class MqttConfig(FrigateBaseModel):
|
|||||||
title="Stats interval",
|
title="Stats interval",
|
||||||
description="Interval in seconds for publishing system and camera stats to MQTT.",
|
description="Interval in seconds for publishing system and camera stats to MQTT.",
|
||||||
)
|
)
|
||||||
user: Optional[EnvString] = Field(
|
user: EnvString | None = Field(
|
||||||
default=None,
|
default=None,
|
||||||
title="MQTT username",
|
title="MQTT username",
|
||||||
description="Optional MQTT username; can be provided via environment variables or secrets.",
|
description="Optional MQTT username; can be provided via environment variables or secrets.",
|
||||||
)
|
)
|
||||||
password: Optional[EnvString] = Field(
|
password: EnvString | None = Field(
|
||||||
default=None,
|
default=None,
|
||||||
title="MQTT password",
|
title="MQTT password",
|
||||||
description="Optional MQTT password; can be provided via environment variables or secrets.",
|
description="Optional MQTT password; can be provided via environment variables or secrets.",
|
||||||
validate_default=True,
|
validate_default=True,
|
||||||
)
|
)
|
||||||
tls_ca_certs: Optional[str] = Field(
|
tls_ca_certs: str | None = Field(
|
||||||
default=None,
|
default=None,
|
||||||
title="TLS CA certs",
|
title="TLS CA certs",
|
||||||
description="Path to CA certificate for TLS connections to the broker (for self-signed certs).",
|
description="Path to CA certificate for TLS connections to the broker (for self-signed certs).",
|
||||||
)
|
)
|
||||||
tls_client_cert: Optional[str] = Field(
|
tls_client_cert: str | None = Field(
|
||||||
default=None,
|
default=None,
|
||||||
title="Client cert",
|
title="Client cert",
|
||||||
description="Client certificate path for TLS mutual authentication; do not set user/password when using client certs.",
|
description="Client certificate path for TLS mutual authentication; do not set user/password when using client certs.",
|
||||||
)
|
)
|
||||||
tls_client_key: Optional[str] = Field(
|
tls_client_key: str | None = Field(
|
||||||
default=None,
|
default=None,
|
||||||
title="Client key",
|
title="Client key",
|
||||||
description="Private key path for the client certificate.",
|
description="Private key path for the client certificate.",
|
||||||
)
|
)
|
||||||
tls_insecure: Optional[bool] = Field(
|
tls_insecure: bool | None = Field(
|
||||||
default=None,
|
default=None,
|
||||||
title="TLS insecure",
|
title="TLS insecure",
|
||||||
description="Allow insecure TLS connections by skipping hostname verification (not recommended).",
|
description="Allow insecure TLS connections by skipping hostname verification (not recommended).",
|
||||||
|
|||||||
@ -1,5 +1,3 @@
|
|||||||
from typing import Union
|
|
||||||
|
|
||||||
from pydantic import Field
|
from pydantic import Field
|
||||||
|
|
||||||
from .base import FrigateBaseModel
|
from .base import FrigateBaseModel
|
||||||
@ -16,12 +14,12 @@ class IPv6Config(FrigateBaseModel):
|
|||||||
|
|
||||||
|
|
||||||
class ListenConfig(FrigateBaseModel):
|
class ListenConfig(FrigateBaseModel):
|
||||||
internal: Union[int, str] = Field(
|
internal: int | str = Field(
|
||||||
default=5000,
|
default=5000,
|
||||||
title="Internal port",
|
title="Internal port",
|
||||||
description="Internal listening port for Frigate (default 5000).",
|
description="Internal listening port for Frigate (default 5000).",
|
||||||
)
|
)
|
||||||
external: Union[int, str] = Field(
|
external: int | str = Field(
|
||||||
default=8971,
|
default=8971,
|
||||||
title="External port",
|
title="External port",
|
||||||
description="External listening port for Frigate (default 8971).",
|
description="External listening port for Frigate (default 8971).",
|
||||||
|
|||||||
@ -3,9 +3,10 @@
|
|||||||
import copy
|
import copy
|
||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
from datetime import datetime, timezone
|
from collections.abc import Callable
|
||||||
|
from datetime import UTC, datetime
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Any, Callable, Optional
|
from typing import Any
|
||||||
|
|
||||||
from frigate.config.camera.updater import (
|
from frigate.config.camera.updater import (
|
||||||
CameraConfigUpdateEnum,
|
CameraConfigUpdateEnum,
|
||||||
@ -169,9 +170,9 @@ class ProfileManager:
|
|||||||
|
|
||||||
def activate_profile(
|
def activate_profile(
|
||||||
self,
|
self,
|
||||||
profile_name: Optional[str],
|
profile_name: str | None,
|
||||||
clear_runtime_overrides: bool = True,
|
clear_runtime_overrides: bool = True,
|
||||||
) -> Optional[str]:
|
) -> str | None:
|
||||||
"""Activate a profile by name, or deactivate if None.
|
"""Activate a profile by name, or deactivate if None.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
@ -256,7 +257,7 @@ class ProfileManager:
|
|||||||
|
|
||||||
def _apply_profile_overrides(
|
def _apply_profile_overrides(
|
||||||
self, profile_name: str, changed: dict[str, set[str]]
|
self, profile_name: str, changed: dict[str, set[str]]
|
||||||
) -> Optional[str]:
|
) -> str | None:
|
||||||
"""Apply profile overrides for all cameras that have the named profile."""
|
"""Apply profile overrides for all cameras that have the named profile."""
|
||||||
for cam_name, cam_config in self.config.cameras.items():
|
for cam_name, cam_config in self.config.cameras.items():
|
||||||
profile = cam_config.profiles.get(profile_name)
|
profile = cam_config.profiles.get(profile_name)
|
||||||
@ -358,14 +359,14 @@ class ProfileManager:
|
|||||||
retain=True,
|
retain=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
def _persist_active_profile(self, profile_name: Optional[str]) -> None:
|
def _persist_active_profile(self, profile_name: str | None) -> None:
|
||||||
"""Persist the active profile state to disk as JSON."""
|
"""Persist the active profile state to disk as JSON."""
|
||||||
try:
|
try:
|
||||||
data = self._load_persisted_data()
|
data = self._load_persisted_data()
|
||||||
data["active"] = profile_name
|
data["active"] = profile_name
|
||||||
if profile_name is not None:
|
if profile_name is not None:
|
||||||
data.setdefault("last_activated", {})[profile_name] = datetime.now(
|
data.setdefault("last_activated", {})[profile_name] = datetime.now(
|
||||||
timezone.utc
|
UTC
|
||||||
).timestamp()
|
).timestamp()
|
||||||
PERSISTENCE_FILE.write_text(json.dumps(data))
|
PERSISTENCE_FILE.write_text(json.dumps(data))
|
||||||
except OSError:
|
except OSError:
|
||||||
@ -384,7 +385,7 @@ class ProfileManager:
|
|||||||
return {"active": None, "last_activated": {}}
|
return {"active": None, "last_activated": {}}
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def load_persisted_profile() -> Optional[str]:
|
def load_persisted_profile() -> str | None:
|
||||||
"""Load the persisted active profile name from disk."""
|
"""Load the persisted active profile name from disk."""
|
||||||
data = ProfileManager._load_persisted_data()
|
data = ProfileManager._load_persisted_data()
|
||||||
name = data.get("active")
|
name = data.get("active")
|
||||||
|
|||||||
@ -1,5 +1,3 @@
|
|||||||
from typing import Optional
|
|
||||||
|
|
||||||
from pydantic import Field, field_validator
|
from pydantic import Field, field_validator
|
||||||
|
|
||||||
from .base import FrigateBaseModel
|
from .base import FrigateBaseModel
|
||||||
@ -19,7 +17,7 @@ class HeaderMappingConfig(FrigateBaseModel):
|
|||||||
title="Role header",
|
title="Role header",
|
||||||
description="Header containing the authenticated user's role or groups from the upstream proxy.",
|
description="Header containing the authenticated user's role or groups from the upstream proxy.",
|
||||||
)
|
)
|
||||||
role_map: Optional[dict[str, list[str]]] = Field(
|
role_map: dict[str, list[str]] | None = Field(
|
||||||
default_factory=dict,
|
default_factory=dict,
|
||||||
title=("Role mapping"),
|
title=("Role mapping"),
|
||||||
description="Map upstream group values to Frigate roles (for example map admin groups to the admin role).",
|
description="Map upstream group values to Frigate roles (for example map admin groups to the admin role).",
|
||||||
@ -32,22 +30,22 @@ class ProxyConfig(FrigateBaseModel):
|
|||||||
title="Header mapping",
|
title="Header mapping",
|
||||||
description="Map incoming proxy headers to Frigate user and role fields for proxy-based auth.",
|
description="Map incoming proxy headers to Frigate user and role fields for proxy-based auth.",
|
||||||
)
|
)
|
||||||
logout_url: Optional[str] = Field(
|
logout_url: str | None = Field(
|
||||||
default=None,
|
default=None,
|
||||||
title="Logout URL",
|
title="Logout URL",
|
||||||
description="URL to redirect users to when logging out via the proxy.",
|
description="URL to redirect users to when logging out via the proxy.",
|
||||||
)
|
)
|
||||||
auth_secret: Optional[EnvString] = Field(
|
auth_secret: EnvString | None = Field(
|
||||||
default=None,
|
default=None,
|
||||||
title="Proxy secret",
|
title="Proxy secret",
|
||||||
description="Optional secret checked against the X-Proxy-Secret header to verify trusted proxies.",
|
description="Optional secret checked against the X-Proxy-Secret header to verify trusted proxies.",
|
||||||
)
|
)
|
||||||
default_role: Optional[str] = Field(
|
default_role: str | None = Field(
|
||||||
default="viewer",
|
default="viewer",
|
||||||
title="Default role",
|
title="Default role",
|
||||||
description="Default role assigned to proxy-authenticated users when no role mapping applies.",
|
description="Default role assigned to proxy-authenticated users when no role mapping applies.",
|
||||||
)
|
)
|
||||||
separator: Optional[str] = Field(
|
separator: str | None = Field(
|
||||||
default=",",
|
default=",",
|
||||||
title="Separator character",
|
title="Separator character",
|
||||||
description="Character used to split multiple values provided in proxy headers.",
|
description="Character used to split multiple values provided in proxy headers.",
|
||||||
|
|||||||
@ -1,5 +1,3 @@
|
|||||||
from typing import Optional
|
|
||||||
|
|
||||||
from pydantic import Field
|
from pydantic import Field
|
||||||
|
|
||||||
from .base import FrigateBaseModel
|
from .base import FrigateBaseModel
|
||||||
@ -23,7 +21,7 @@ class StatsConfig(FrigateBaseModel):
|
|||||||
title="Network bandwidth",
|
title="Network bandwidth",
|
||||||
description="Enable per-process network bandwidth monitoring for camera ffmpeg processes and detectors (requires capabilities).",
|
description="Enable per-process network bandwidth monitoring for camera ffmpeg processes and detectors (requires capabilities).",
|
||||||
)
|
)
|
||||||
intel_gpu_device: Optional[str] = Field(
|
intel_gpu_device: str | None = Field(
|
||||||
default=None,
|
default=None,
|
||||||
title="Intel GPU device",
|
title="Intel GPU device",
|
||||||
description="PCI bus address or DRM device path (e.g. /dev/dri/card1) used to pin Intel GPU stats to a specific device when multiple are present.",
|
description="PCI bus address or DRM device path (e.g. /dev/dri/card1) used to pin Intel GPU stats to a specific device when multiple are present.",
|
||||||
|
|||||||
@ -1,5 +1,4 @@
|
|||||||
from enum import Enum
|
from enum import Enum
|
||||||
from typing import Optional
|
|
||||||
|
|
||||||
from pydantic import Field
|
from pydantic import Field
|
||||||
|
|
||||||
@ -20,7 +19,7 @@ class UnitSystemEnum(str, Enum):
|
|||||||
|
|
||||||
|
|
||||||
class UIConfig(FrigateBaseModel):
|
class UIConfig(FrigateBaseModel):
|
||||||
timezone: Optional[str] = Field(
|
timezone: str | None = Field(
|
||||||
default=None,
|
default=None,
|
||||||
title="Timezone",
|
title="Timezone",
|
||||||
description="Optional timezone to display across the UI (defaults to browser local time if unset).",
|
description="Optional timezone to display across the UI (defaults to browser local time if unset).",
|
||||||
|
|||||||
@ -10,7 +10,7 @@ import random
|
|||||||
import re
|
import re
|
||||||
import string
|
import string
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Any, List, Tuple
|
from typing import Any
|
||||||
|
|
||||||
import cv2
|
import cv2
|
||||||
import numpy as np
|
import numpy as np
|
||||||
@ -86,7 +86,7 @@ class LicensePlateProcessingMixin:
|
|||||||
self.similarity_threshold = 0.8
|
self.similarity_threshold = 0.8
|
||||||
self.cluster_threshold = 0.85
|
self.cluster_threshold = 0.85
|
||||||
|
|
||||||
def _detect(self, image: np.ndarray, debug_frame_id: int) -> List[np.ndarray]:
|
def _detect(self, image: np.ndarray, debug_frame_id: int) -> list[np.ndarray]:
|
||||||
"""
|
"""
|
||||||
Detect possible areas of text in the input image by first resizing and normalizing it,
|
Detect possible areas of text in the input image by first resizing and normalizing it,
|
||||||
running a detection model, and filtering out low-probability regions.
|
running a detection model, and filtering out low-probability regions.
|
||||||
@ -132,8 +132,8 @@ class LicensePlateProcessingMixin:
|
|||||||
return self._filter_polygon(boxes, (h, w)) # type: ignore[return-value,arg-type]
|
return self._filter_polygon(boxes, (h, w)) # type: ignore[return-value,arg-type]
|
||||||
|
|
||||||
def _classify(
|
def _classify(
|
||||||
self, images: List[np.ndarray]
|
self, images: list[np.ndarray]
|
||||||
) -> Tuple[List[np.ndarray], List[Tuple[str, float]]] | None:
|
) -> tuple[list[np.ndarray], list[tuple[str, float]]] | None:
|
||||||
"""
|
"""
|
||||||
Classify the orientation or category of each detected license plate.
|
Classify the orientation or category of each detected license plate.
|
||||||
|
|
||||||
@ -163,8 +163,8 @@ class LicensePlateProcessingMixin:
|
|||||||
return self._process_classification_output(images, outputs)
|
return self._process_classification_output(images, outputs)
|
||||||
|
|
||||||
def _recognize(
|
def _recognize(
|
||||||
self, camera: str, images: List[np.ndarray]
|
self, camera: str, images: list[np.ndarray]
|
||||||
) -> Tuple[List[str], List[List[float]]]:
|
) -> tuple[list[str], list[list[float]]]:
|
||||||
"""
|
"""
|
||||||
Recognize the characters on the detected license plates using the recognition model.
|
Recognize the characters on the detected license plates using the recognition model.
|
||||||
|
|
||||||
@ -205,7 +205,7 @@ class LicensePlateProcessingMixin:
|
|||||||
|
|
||||||
def _process_license_plate(
|
def _process_license_plate(
|
||||||
self, camera: str, id: str, image: np.ndarray, debug_frame_id: int
|
self, camera: str, id: str, image: np.ndarray, debug_frame_id: int
|
||||||
) -> Tuple[List[str], List[List[float]], List[int]]:
|
) -> tuple[list[str], list[list[float]], list[int]]:
|
||||||
"""
|
"""
|
||||||
Complete pipeline for detecting, classifying, and recognizing license plates in the input image.
|
Complete pipeline for detecting, classifying, and recognizing license plates in the input image.
|
||||||
Combines multi-line plates into a single plate string, grouping boxes by vertical alignment and ordering top to bottom,
|
Combines multi-line plates into a single plate string, grouping boxes by vertical alignment and ordering top to bottom,
|
||||||
@ -469,11 +469,11 @@ class LicensePlateProcessingMixin:
|
|||||||
|
|
||||||
def _merge_nearby_boxes(
|
def _merge_nearby_boxes(
|
||||||
self,
|
self,
|
||||||
boxes: List[np.ndarray],
|
boxes: list[np.ndarray],
|
||||||
plate_width: float,
|
plate_width: float,
|
||||||
gap_fraction: float = 0.1,
|
gap_fraction: float = 0.1,
|
||||||
min_overlap_fraction: float = -0.2,
|
min_overlap_fraction: float = -0.2,
|
||||||
) -> List[np.ndarray]:
|
) -> list[np.ndarray]:
|
||||||
"""
|
"""
|
||||||
Merge bounding boxes that are likely part of the same license plate based on proximity,
|
Merge bounding boxes that are likely part of the same license plate based on proximity,
|
||||||
with a dynamic max_gap based on the provided width of the entire license plate.
|
with a dynamic max_gap based on the provided width of the entire license plate.
|
||||||
@ -555,7 +555,7 @@ class LicensePlateProcessingMixin:
|
|||||||
|
|
||||||
def _boxes_from_bitmap(
|
def _boxes_from_bitmap(
|
||||||
self, output: np.ndarray, mask: np.ndarray, dest_width: int, dest_height: int
|
self, output: np.ndarray, mask: np.ndarray, dest_width: int, dest_height: int
|
||||||
) -> Tuple[np.ndarray, List[float]]:
|
) -> tuple[np.ndarray, list[float]]:
|
||||||
"""
|
"""
|
||||||
Process the binary mask to extract bounding boxes and associated confidence scores.
|
Process the binary mask to extract bounding boxes and associated confidence scores.
|
||||||
|
|
||||||
@ -620,7 +620,7 @@ class LicensePlateProcessingMixin:
|
|||||||
return np.array(boxes, dtype="int32"), scores
|
return np.array(boxes, dtype="int32"), scores
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _get_min_boxes(contour: np.ndarray) -> Tuple[List[Tuple[float, float]], float]:
|
def _get_min_boxes(contour: np.ndarray) -> tuple[list[tuple[float, float]], float]:
|
||||||
"""
|
"""
|
||||||
Calculate the minimum bounding box (rotated rectangle) for a given contour.
|
Calculate the minimum bounding box (rotated rectangle) for a given contour.
|
||||||
|
|
||||||
@ -659,7 +659,7 @@ class LicensePlateProcessingMixin:
|
|||||||
return cv2.mean(bitmap[y1 : y2 + 1, x1 : x2 + 1], mask)[0]
|
return cv2.mean(bitmap[y1 : y2 + 1, x1 : x2 + 1], mask)[0]
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _expand_box(points: List[Tuple[float, float]]) -> np.ndarray:
|
def _expand_box(points: list[tuple[float, float]]) -> np.ndarray:
|
||||||
"""
|
"""
|
||||||
Expand a polygonal shape slightly by a factor determined by the area-to-perimeter ratio.
|
Expand a polygonal shape slightly by a factor determined by the area-to-perimeter ratio.
|
||||||
|
|
||||||
@ -677,7 +677,7 @@ class LicensePlateProcessingMixin:
|
|||||||
return expanded
|
return expanded
|
||||||
|
|
||||||
def _filter_polygon(
|
def _filter_polygon(
|
||||||
self, points: List[np.ndarray], shape: Tuple[int, int]
|
self, points: list[np.ndarray], shape: tuple[int, int]
|
||||||
) -> np.ndarray:
|
) -> np.ndarray:
|
||||||
"""
|
"""
|
||||||
Filter a set of polygons to include only valid ones that fit within an image shape
|
Filter a set of polygons to include only valid ones that fit within an image shape
|
||||||
@ -839,8 +839,8 @@ class LicensePlateProcessingMixin:
|
|||||||
return padded_image
|
return padded_image
|
||||||
|
|
||||||
def _process_classification_output(
|
def _process_classification_output(
|
||||||
self, images: List[np.ndarray], outputs: List[np.ndarray]
|
self, images: list[np.ndarray], outputs: list[np.ndarray]
|
||||||
) -> Tuple[List[np.ndarray], List[Tuple[str, float]]]:
|
) -> tuple[list[np.ndarray], list[tuple[str, float]]]:
|
||||||
"""
|
"""
|
||||||
Process the classification model output by matching labels with confidence scores.
|
Process the classification model output by matching labels with confidence scores.
|
||||||
|
|
||||||
@ -1095,8 +1095,8 @@ class LicensePlateProcessingMixin:
|
|||||||
return None # No detection above the threshold
|
return None # No detection above the threshold
|
||||||
|
|
||||||
def _get_cluster_rep(
|
def _get_cluster_rep(
|
||||||
self, plates: List[dict]
|
self, plates: list[dict]
|
||||||
) -> Tuple[str, float, List[float], int]:
|
) -> tuple[str, float, list[float], int]:
|
||||||
"""
|
"""
|
||||||
Cluster plate variants and select the representative from the best cluster.
|
Cluster plate variants and select the representative from the best cluster.
|
||||||
"""
|
"""
|
||||||
@ -1704,7 +1704,7 @@ class CTCDecoder:
|
|||||||
"""
|
"""
|
||||||
self.characters = []
|
self.characters = []
|
||||||
if character_dict_path and os.path.exists(character_dict_path):
|
if character_dict_path and os.path.exists(character_dict_path):
|
||||||
with open(character_dict_path, "r", encoding="utf-8") as f:
|
with open(character_dict_path, encoding="utf-8") as f:
|
||||||
self.characters = (
|
self.characters = (
|
||||||
["blank"] + [line.strip() for line in f if line.strip()] + [" "]
|
["blank"] + [line.strip() for line in f if line.strip()] + [" "]
|
||||||
)
|
)
|
||||||
@ -1812,8 +1812,8 @@ class CTCDecoder:
|
|||||||
self.char_map = {i: char for i, char in enumerate(self.characters)}
|
self.char_map = {i: char for i, char in enumerate(self.characters)}
|
||||||
|
|
||||||
def __call__(
|
def __call__(
|
||||||
self, outputs: List[np.ndarray]
|
self, outputs: list[np.ndarray]
|
||||||
) -> Tuple[List[str], List[List[float]]]:
|
) -> tuple[list[str], list[list[float]]]:
|
||||||
"""
|
"""
|
||||||
Decode a batch of model outputs into character sequences and their confidence scores.
|
Decode a batch of model outputs into character sequences and their confidence scores.
|
||||||
|
|
||||||
|
|||||||
@ -4,7 +4,7 @@ import logging
|
|||||||
import os
|
import os
|
||||||
import threading
|
import threading
|
||||||
import time
|
import time
|
||||||
from typing import Any, Optional
|
from typing import Any
|
||||||
|
|
||||||
from peewee import DoesNotExist
|
from peewee import DoesNotExist
|
||||||
|
|
||||||
@ -142,7 +142,7 @@ class AudioTranscriptionPostProcessor(PostProcessorApi):
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Error in audio transcription post-processing: {e}")
|
logger.error(f"Error in audio transcription post-processing: {e}")
|
||||||
|
|
||||||
def __transcribe_audio(self, audio_data: bytes) -> Optional[str]:
|
def __transcribe_audio(self, audio_data: bytes) -> str | None:
|
||||||
"""Transcribe WAV audio data using faster-whisper."""
|
"""Transcribe WAV audio data using faster-whisper."""
|
||||||
if not self.recognizer:
|
if not self.recognizer:
|
||||||
logger.debug("Recognizer not initialized")
|
logger.debug("Recognizer not initialized")
|
||||||
@ -168,8 +168,9 @@ class AudioTranscriptionPostProcessor(PostProcessorApi):
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
logger.debug(
|
logger.debug(
|
||||||
"Detected language '%s' with probability %f"
|
"Detected language '%s' with probability %f",
|
||||||
% (info.language, info.language_probability)
|
info.language,
|
||||||
|
info.language_probability,
|
||||||
)
|
)
|
||||||
|
|
||||||
return text
|
return text
|
||||||
|
|||||||
@ -102,11 +102,9 @@ class LicensePlatePostProcessor(LicensePlateProcessingMixin, PostProcessorApi):
|
|||||||
Recordings.start_time,
|
Recordings.start_time,
|
||||||
)
|
)
|
||||||
.where(
|
.where(
|
||||||
(
|
|
||||||
(frame_time >= Recordings.start_time)
|
(frame_time >= Recordings.start_time)
|
||||||
& (frame_time <= Recordings.end_time)
|
& (frame_time <= Recordings.end_time)
|
||||||
)
|
)
|
||||||
)
|
|
||||||
.where(Recordings.camera == camera_name)
|
.where(Recordings.camera == camera_name)
|
||||||
.order_by(Recordings.start_time.desc())
|
.order_by(Recordings.start_time.desc())
|
||||||
.limit(1)
|
.limit(1)
|
||||||
|
|||||||
@ -55,7 +55,7 @@ class SemanticTriggerProcessor(PostProcessorApi):
|
|||||||
|
|
||||||
# load stats from disk
|
# load stats from disk
|
||||||
try:
|
try:
|
||||||
with open(os.path.join(CONFIG_DIR, ".search_stats.json"), "r") as f:
|
with open(os.path.join(CONFIG_DIR, ".search_stats.json")) as f:
|
||||||
data = json.loads(f.read())
|
data = json.loads(f.read())
|
||||||
self.thumb_stats.from_dict(data["thumb_stats"])
|
self.thumb_stats.from_dict(data["thumb_stats"])
|
||||||
self.desc_stats.from_dict(data["desc_stats"])
|
self.desc_stats.from_dict(data["desc_stats"])
|
||||||
|
|||||||
@ -4,9 +4,10 @@ import logging
|
|||||||
import threading
|
import threading
|
||||||
from abc import ABC, abstractmethod
|
from abc import ABC, abstractmethod
|
||||||
from collections import deque
|
from collections import deque
|
||||||
|
from collections.abc import Callable
|
||||||
from concurrent.futures import Future
|
from concurrent.futures import Future
|
||||||
from queue import Empty, Full, Queue
|
from queue import Empty, Full, Queue
|
||||||
from typing import Any, Callable
|
from typing import Any
|
||||||
|
|
||||||
import numpy as np
|
import numpy as np
|
||||||
|
|
||||||
|
|||||||
@ -4,7 +4,7 @@ import logging
|
|||||||
import os
|
import os
|
||||||
import queue
|
import queue
|
||||||
import threading
|
import threading
|
||||||
from typing import Any, Optional
|
from typing import Any
|
||||||
|
|
||||||
import numpy as np
|
import numpy as np
|
||||||
|
|
||||||
@ -75,9 +75,7 @@ class AudioTranscriptionRealTimeProcessor(RealTimeProcessorApi):
|
|||||||
f"Failed to initialize live streaming audio transcription: {e}"
|
f"Failed to initialize live streaming audio transcription: {e}"
|
||||||
)
|
)
|
||||||
|
|
||||||
def __process_audio_stream(
|
def __process_audio_stream(self, audio_data: np.ndarray) -> tuple[str, bool] | None:
|
||||||
self, audio_data: np.ndarray
|
|
||||||
) -> Optional[tuple[str, bool]]:
|
|
||||||
if (
|
if (
|
||||||
self.model_runner.model is None
|
self.model_runner.model is None
|
||||||
and self.config.audio_transcription.model_size == "small"
|
and self.config.audio_transcription.model_size == "small"
|
||||||
|
|||||||
@ -7,7 +7,7 @@ import logging
|
|||||||
import os
|
import os
|
||||||
import shutil
|
import shutil
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Any, Optional
|
from typing import Any
|
||||||
|
|
||||||
import cv2
|
import cv2
|
||||||
import numpy as np
|
import numpy as np
|
||||||
@ -219,7 +219,7 @@ class FaceRealTimeProcessor(RealTimeProcessorApi):
|
|||||||
logger.debug("Not processing due to hitting max rec attempts.")
|
logger.debug("Not processing due to hitting max rec attempts.")
|
||||||
return
|
return
|
||||||
|
|
||||||
face: Optional[dict[str, Any]] = None
|
face: dict[str, Any] | None = None
|
||||||
|
|
||||||
if self.requires_face_detection:
|
if self.requires_face_detection:
|
||||||
logger.debug("Running manual face detection.")
|
logger.debug("Running manual face detection.")
|
||||||
|
|||||||
@ -1053,7 +1053,7 @@ if __name__ == "__main__":
|
|||||||
|
|
||||||
SAMPLING_RATE = 16000
|
SAMPLING_RATE = 16000
|
||||||
duration = len(load_audio(audio_path)) / SAMPLING_RATE
|
duration = len(load_audio(audio_path)) / SAMPLING_RATE
|
||||||
logger.info("Audio duration is: %2.2f seconds" % duration)
|
logger.info("Audio duration is: %2.2f seconds", duration)
|
||||||
|
|
||||||
asr, online = asr_factory(args, logfile=logfile)
|
asr, online = asr_factory(args, logfile=logfile)
|
||||||
if args.vac:
|
if args.vac:
|
||||||
|
|||||||
@ -131,7 +131,7 @@ class DebugReplayManager:
|
|||||||
|
|
||||||
config_file = find_config_file()
|
config_file = find_config_file()
|
||||||
yaml_parser = YAML()
|
yaml_parser = YAML()
|
||||||
with open(config_file, "r") as f:
|
with open(config_file) as f:
|
||||||
config_data = yaml_parser.load(f)
|
config_data = yaml_parser.load(f)
|
||||||
|
|
||||||
if "cameras" not in config_data or config_data["cameras"] is None:
|
if "cameras" not in config_data or config_data["cameras"] is None:
|
||||||
|
|||||||
@ -1,6 +1,5 @@
|
|||||||
import logging
|
import logging
|
||||||
from abc import ABC, abstractmethod
|
from abc import ABC, abstractmethod
|
||||||
from typing import List
|
|
||||||
|
|
||||||
import numpy as np
|
import numpy as np
|
||||||
|
|
||||||
@ -11,7 +10,7 @@ logger = logging.getLogger(__name__)
|
|||||||
|
|
||||||
class DetectionApi(ABC):
|
class DetectionApi(ABC):
|
||||||
type_key: str
|
type_key: str
|
||||||
supported_models: List[ModelTypeEnum]
|
supported_models: list[ModelTypeEnum]
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def __init__(self, detector_config: BaseDetectorConfig):
|
def __init__(self, detector_config: BaseDetectorConfig):
|
||||||
|
|||||||
@ -491,7 +491,7 @@ class RKNNModelRunner(BaseModelRunner):
|
|||||||
|
|
||||||
except ImportError:
|
except ImportError:
|
||||||
logger.error("RKNN Lite not available")
|
logger.error("RKNN Lite not available")
|
||||||
raise ImportError("RKNN Lite not available")
|
raise ImportError("RKNN Lite not available") from None
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Error loading RKNN model: {e}")
|
logger.error(f"Error loading RKNN model: {e}")
|
||||||
raise
|
raise
|
||||||
|
|||||||
@ -3,7 +3,7 @@ import json
|
|||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
from enum import Enum
|
from enum import Enum
|
||||||
from typing import Any, Dict, Optional, Tuple
|
from typing import Any
|
||||||
|
|
||||||
import requests
|
import requests
|
||||||
from pydantic import BaseModel, ConfigDict, Field
|
from pydantic import BaseModel, ConfigDict, Field
|
||||||
@ -45,12 +45,12 @@ class ModelTypeEnum(str, Enum):
|
|||||||
|
|
||||||
|
|
||||||
class ModelConfig(BaseModel):
|
class ModelConfig(BaseModel):
|
||||||
path: Optional[str] = Field(
|
path: str | None = Field(
|
||||||
None,
|
None,
|
||||||
title="Custom object detector model path",
|
title="Custom object detector model path",
|
||||||
description="Path to a custom detection model file (or plus://<model_id> for Frigate+ models).",
|
description="Path to a custom detection model file (or plus://<model_id> for Frigate+ models).",
|
||||||
)
|
)
|
||||||
labelmap_path: Optional[str] = Field(
|
labelmap_path: str | None = Field(
|
||||||
None,
|
None,
|
||||||
title="Label map for custom object detector",
|
title="Label map for custom object detector",
|
||||||
description="Path to a labelmap file that maps numeric classes to string labels for the detector.",
|
description="Path to a labelmap file that maps numeric classes to string labels for the detector.",
|
||||||
@ -65,12 +65,12 @@ class ModelConfig(BaseModel):
|
|||||||
title="Object detection model input height",
|
title="Object detection model input height",
|
||||||
description="Height of the model input tensor in pixels.",
|
description="Height of the model input tensor in pixels.",
|
||||||
)
|
)
|
||||||
labelmap: Dict[int, str] = Field(
|
labelmap: dict[int, str] = Field(
|
||||||
default_factory=dict,
|
default_factory=dict,
|
||||||
title="Labelmap customization",
|
title="Labelmap customization",
|
||||||
description="Overrides or remapping entries to merge into the standard labelmap.",
|
description="Overrides or remapping entries to merge into the standard labelmap.",
|
||||||
)
|
)
|
||||||
attributes_map: Dict[str, list[str]] = Field(
|
attributes_map: dict[str, list[str]] = Field(
|
||||||
default=DEFAULT_ATTRIBUTE_LABEL_MAP,
|
default=DEFAULT_ATTRIBUTE_LABEL_MAP,
|
||||||
title="Map of object labels to their attribute labels",
|
title="Map of object labels to their attribute labels",
|
||||||
description="Mapping from object labels to attribute labels used to attach metadata (for example 'car' -> ['license_plate']).",
|
description="Mapping from object labels to attribute labels used to attach metadata (for example 'car' -> ['license_plate']).",
|
||||||
@ -95,18 +95,18 @@ class ModelConfig(BaseModel):
|
|||||||
title="Object Detection Model Type",
|
title="Object Detection Model Type",
|
||||||
description="Detector model architecture type (ssd, yolox, yolonas) used by some detectors for optimization.",
|
description="Detector model architecture type (ssd, yolox, yolonas) used by some detectors for optimization.",
|
||||||
)
|
)
|
||||||
_merged_labelmap: Optional[Dict[int, str]] = PrivateAttr()
|
_merged_labelmap: dict[int, str] | None = PrivateAttr()
|
||||||
_colormap: Dict[int, Tuple[int, int, int]] = PrivateAttr()
|
_colormap: dict[int, tuple[int, int, int]] = PrivateAttr()
|
||||||
_all_attributes: list[str] = PrivateAttr()
|
_all_attributes: list[str] = PrivateAttr()
|
||||||
_all_attribute_logos: list[str] = PrivateAttr()
|
_all_attribute_logos: list[str] = PrivateAttr()
|
||||||
_model_hash: str = PrivateAttr()
|
_model_hash: str = PrivateAttr()
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def merged_labelmap(self) -> Dict[int, str]:
|
def merged_labelmap(self) -> dict[int, str]:
|
||||||
return self._merged_labelmap
|
return self._merged_labelmap
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def colormap(self) -> Dict[int, Tuple[int, int, int]]:
|
def colormap(self) -> dict[int, tuple[int, int, int]]:
|
||||||
return self._colormap
|
return self._colormap
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@ -171,7 +171,7 @@ class ModelConfig(BaseModel):
|
|||||||
with open(model_info_path, "w") as f:
|
with open(model_info_path, "w") as f:
|
||||||
json.dump(model_info, f)
|
json.dump(model_info, f)
|
||||||
else:
|
else:
|
||||||
with open(model_info_path, "r") as f:
|
with open(model_info_path) as f:
|
||||||
model_info: dict[str, Any] = json.load(f)
|
model_info: dict[str, Any] = json.load(f)
|
||||||
|
|
||||||
if detector and detector not in model_info["supportedDetectors"]:
|
if detector and detector not in model_info["supportedDetectors"]:
|
||||||
@ -240,12 +240,12 @@ class BaseDetectorConfig(BaseModel):
|
|||||||
title="Detector Type",
|
title="Detector Type",
|
||||||
description="Type of detector to use for object detection (for example 'cpu', 'edgetpu', 'openvino').",
|
description="Type of detector to use for object detection (for example 'cpu', 'edgetpu', 'openvino').",
|
||||||
)
|
)
|
||||||
model: Optional[ModelConfig] = Field(
|
model: ModelConfig | None = Field(
|
||||||
default=None,
|
default=None,
|
||||||
title="Detector specific model configuration",
|
title="Detector specific model configuration",
|
||||||
description="Detector-specific model configuration options (path, input size, etc.).",
|
description="Detector-specific model configuration options (path, input size, etc.).",
|
||||||
)
|
)
|
||||||
model_path: Optional[str] = Field(
|
model_path: str | None = Field(
|
||||||
default=None,
|
default=None,
|
||||||
title="Detector specific model path",
|
title="Detector specific model path",
|
||||||
description="File path to the detector model binary if required by the chosen detector.",
|
description="File path to the detector model binary if required by the chosen detector.",
|
||||||
|
|||||||
@ -2,10 +2,9 @@ import importlib
|
|||||||
import logging
|
import logging
|
||||||
import pkgutil
|
import pkgutil
|
||||||
from enum import Enum
|
from enum import Enum
|
||||||
from typing import Union
|
from typing import Annotated, Union
|
||||||
|
|
||||||
from pydantic import Field
|
from pydantic import Field
|
||||||
from typing_extensions import Annotated
|
|
||||||
|
|
||||||
from . import plugins
|
from . import plugins
|
||||||
from .detection_api import DetectionApi
|
from .detection_api import DetectionApi
|
||||||
@ -37,6 +36,6 @@ class StrEnum(str, Enum):
|
|||||||
DetectorTypeEnum = StrEnum("DetectorTypeEnum", {k: k for k in api_types})
|
DetectorTypeEnum = StrEnum("DetectorTypeEnum", {k: k for k in api_types})
|
||||||
|
|
||||||
DetectorConfig = Annotated[
|
DetectorConfig = Annotated[
|
||||||
Union[tuple(BaseDetectorConfig.__subclasses__())],
|
Union[tuple(BaseDetectorConfig.__subclasses__())], # noqa: UP007
|
||||||
Field(discriminator="type"),
|
Field(discriminator="type"),
|
||||||
]
|
]
|
||||||
|
|||||||
@ -39,8 +39,7 @@ class Axengine(DetectionApi):
|
|||||||
try:
|
try:
|
||||||
import axengine as axe
|
import axengine as axe
|
||||||
except ModuleNotFoundError:
|
except ModuleNotFoundError:
|
||||||
raise ImportError("AXEngine is not installed.")
|
raise ImportError("AXEngine is not installed.") from None
|
||||||
return
|
|
||||||
|
|
||||||
logger.info("__init__ axengine")
|
logger.info("__init__ axengine")
|
||||||
super().__init__(config)
|
super().__init__(config)
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
import logging
|
import logging
|
||||||
|
from typing import Literal
|
||||||
|
|
||||||
from pydantic import ConfigDict, Field
|
from pydantic import ConfigDict, Field
|
||||||
from typing_extensions import Literal
|
|
||||||
|
|
||||||
from frigate.detectors.detection_api import DetectionApi
|
from frigate.detectors.detection_api import DetectionApi
|
||||||
from frigate.detectors.detector_config import BaseDetectorConfig
|
from frigate.detectors.detector_config import BaseDetectorConfig
|
||||||
|
|||||||
@ -1,11 +1,11 @@
|
|||||||
import io
|
import io
|
||||||
import logging
|
import logging
|
||||||
|
from typing import Literal
|
||||||
|
|
||||||
import numpy as np
|
import numpy as np
|
||||||
import requests
|
import requests
|
||||||
from PIL import Image
|
from PIL import Image
|
||||||
from pydantic import ConfigDict, Field
|
from pydantic import ConfigDict, Field
|
||||||
from typing_extensions import Literal
|
|
||||||
|
|
||||||
from frigate.detectors.detection_api import DetectionApi
|
from frigate.detectors.detection_api import DetectionApi
|
||||||
from frigate.detectors.detector_config import BaseDetectorConfig
|
from frigate.detectors.detector_config import BaseDetectorConfig
|
||||||
|
|||||||
@ -1,9 +1,9 @@
|
|||||||
import logging
|
import logging
|
||||||
import queue
|
import queue
|
||||||
|
from typing import Literal
|
||||||
|
|
||||||
import numpy as np
|
import numpy as np
|
||||||
from pydantic import ConfigDict, Field
|
from pydantic import ConfigDict, Field
|
||||||
from typing_extensions import Literal
|
|
||||||
|
|
||||||
from frigate.detectors.detection_api import DetectionApi
|
from frigate.detectors.detection_api import DetectionApi
|
||||||
from frigate.detectors.detector_config import BaseDetectorConfig
|
from frigate.detectors.detector_config import BaseDetectorConfig
|
||||||
@ -46,7 +46,7 @@ class DGDetector(DetectionApi):
|
|||||||
try:
|
try:
|
||||||
import degirum as dg
|
import degirum as dg
|
||||||
except ModuleNotFoundError:
|
except ModuleNotFoundError:
|
||||||
raise ImportError("Unable to import DeGirum detector.")
|
raise ImportError("Unable to import DeGirum detector.") from None
|
||||||
|
|
||||||
self._queue = queue.Queue()
|
self._queue = queue.Queue()
|
||||||
self._zoo = dg.connect(
|
self._zoo = dg.connect(
|
||||||
|
|||||||
@ -1,11 +1,11 @@
|
|||||||
import logging
|
import logging
|
||||||
import math
|
import math
|
||||||
import os
|
import os
|
||||||
|
from typing import Literal
|
||||||
|
|
||||||
import cv2
|
import cv2
|
||||||
import numpy as np
|
import numpy as np
|
||||||
from pydantic import ConfigDict, Field
|
from pydantic import ConfigDict, Field
|
||||||
from typing_extensions import Literal
|
|
||||||
|
|
||||||
from frigate.detectors.detection_api import DetectionApi
|
from frigate.detectors.detection_api import DetectionApi
|
||||||
from frigate.detectors.detector_config import BaseDetectorConfig, ModelTypeEnum
|
from frigate.detectors.detector_config import BaseDetectorConfig, ModelTypeEnum
|
||||||
|
|||||||
@ -4,12 +4,11 @@ import subprocess
|
|||||||
import threading
|
import threading
|
||||||
import urllib.request
|
import urllib.request
|
||||||
from functools import partial
|
from functools import partial
|
||||||
from typing import Dict, List, Optional, Tuple
|
from typing import Literal
|
||||||
|
|
||||||
import cv2
|
import cv2
|
||||||
import numpy as np
|
import numpy as np
|
||||||
from pydantic import ConfigDict, Field
|
from pydantic import ConfigDict, Field
|
||||||
from typing_extensions import Literal
|
|
||||||
|
|
||||||
from frigate.const import MODEL_CACHE_DIR
|
from frigate.const import MODEL_CACHE_DIR
|
||||||
from frigate.detectors.detection_api import DetectionApi
|
from frigate.detectors.detection_api import DetectionApi
|
||||||
@ -83,8 +82,8 @@ class HailoAsyncInference:
|
|||||||
input_store: RequestStore,
|
input_store: RequestStore,
|
||||||
output_store: ResponseStore,
|
output_store: ResponseStore,
|
||||||
batch_size: int = 1,
|
batch_size: int = 1,
|
||||||
input_type: Optional[str] = None,
|
input_type: str | None = None,
|
||||||
output_type: Optional[Dict[str, str]] = None,
|
output_type: dict[str, str] | None = None,
|
||||||
send_original_frame: bool = False,
|
send_original_frame: bool = False,
|
||||||
) -> None:
|
) -> None:
|
||||||
# when importing hailo it activates the driver
|
# when importing hailo it activates the driver
|
||||||
@ -125,9 +124,9 @@ class HailoAsyncInference:
|
|||||||
def callback(
|
def callback(
|
||||||
self,
|
self,
|
||||||
completion_info,
|
completion_info,
|
||||||
bindings_list: List,
|
bindings_list: list,
|
||||||
input_batch: List,
|
input_batch: list,
|
||||||
request_ids: List[int],
|
request_ids: list[int],
|
||||||
):
|
):
|
||||||
if completion_info.exception:
|
if completion_info.exception:
|
||||||
logger.error(f"Inference error: {completion_info.exception}")
|
logger.error(f"Inference error: {completion_info.exception}")
|
||||||
@ -163,7 +162,7 @@ class HailoAsyncInference:
|
|||||||
}
|
}
|
||||||
return configured_infer_model.create_bindings(output_buffers=output_buffers)
|
return configured_infer_model.create_bindings(output_buffers=output_buffers)
|
||||||
|
|
||||||
def get_input_shape(self) -> Tuple[int, ...]:
|
def get_input_shape(self) -> tuple[int, ...]:
|
||||||
return self.hef.get_input_vstream_infos()[0].shape
|
return self.hef.get_input_vstream_infos()[0].shape
|
||||||
|
|
||||||
def run(self) -> None:
|
def run(self) -> None:
|
||||||
@ -304,7 +303,7 @@ class HailoDetector(DetectionApi):
|
|||||||
urllib.request.urlretrieve(url, destination)
|
urllib.request.urlretrieve(url, destination)
|
||||||
logger.debug(f"Downloaded model to {destination}")
|
logger.debug(f"Downloaded model to {destination}")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
raise RuntimeError(f"Failed to download model from {url}: {str(e)}")
|
raise RuntimeError(f"Failed to download model from {url}: {str(e)}") from e
|
||||||
|
|
||||||
def check_and_prepare(self) -> str:
|
def check_and_prepare(self) -> str:
|
||||||
if not os.path.exists(self.cache_dir):
|
if not os.path.exists(self.cache_dir):
|
||||||
@ -350,7 +349,7 @@ class HailoDetector(DetectionApi):
|
|||||||
if not self.inference_thread.is_alive():
|
if not self.inference_thread.is_alive():
|
||||||
raise RuntimeError(
|
raise RuntimeError(
|
||||||
"HailoRT inference thread has stopped, restart required."
|
"HailoRT inference thread has stopped, restart required."
|
||||||
)
|
) from None
|
||||||
|
|
||||||
return np.zeros((20, 6), dtype=np.float32)
|
return np.zeros((20, 6), dtype=np.float32)
|
||||||
|
|
||||||
|
|||||||
@ -5,11 +5,11 @@ import shutil
|
|||||||
import urllib.request
|
import urllib.request
|
||||||
import zipfile
|
import zipfile
|
||||||
from queue import Queue
|
from queue import Queue
|
||||||
|
from typing import Literal
|
||||||
|
|
||||||
import cv2
|
import cv2
|
||||||
import numpy as np
|
import numpy as np
|
||||||
from pydantic import BaseModel, ConfigDict, Field
|
from pydantic import BaseModel, ConfigDict, Field
|
||||||
from typing_extensions import Literal
|
|
||||||
|
|
||||||
from frigate.detectors.detection_api import DetectionApi
|
from frigate.detectors.detection_api import DetectionApi
|
||||||
from frigate.detectors.detector_config import (
|
from frigate.detectors.detector_config import (
|
||||||
@ -61,7 +61,7 @@ class MemryXDetector(DetectionApi):
|
|||||||
except ModuleNotFoundError:
|
except ModuleNotFoundError:
|
||||||
raise ImportError(
|
raise ImportError(
|
||||||
"MemryX SDK is not installed. Install it and set up MIX environment."
|
"MemryX SDK is not installed. Install it and set up MIX environment."
|
||||||
)
|
) from None
|
||||||
return
|
return
|
||||||
|
|
||||||
# Initialize stop_event as None, will be set later by set_stop_event()
|
# Initialize stop_event as None, will be set later by set_stop_event()
|
||||||
|
|||||||
@ -1,8 +1,8 @@
|
|||||||
import logging
|
import logging
|
||||||
|
from typing import Literal
|
||||||
|
|
||||||
import numpy as np
|
import numpy as np
|
||||||
from pydantic import ConfigDict, Field
|
from pydantic import ConfigDict, Field
|
||||||
from typing_extensions import Literal
|
|
||||||
|
|
||||||
from frigate.detectors.detection_api import DetectionApi
|
from frigate.detectors.detection_api import DetectionApi
|
||||||
from frigate.detectors.detection_runners import get_optimized_runner
|
from frigate.detectors.detection_runners import get_optimized_runner
|
||||||
|
|||||||
@ -1,9 +1,9 @@
|
|||||||
import logging
|
import logging
|
||||||
|
from typing import Literal
|
||||||
|
|
||||||
import numpy as np
|
import numpy as np
|
||||||
import openvino as ov
|
import openvino as ov
|
||||||
from pydantic import ConfigDict, Field
|
from pydantic import ConfigDict, Field
|
||||||
from typing_extensions import Literal
|
|
||||||
|
|
||||||
from frigate.detectors.detection_api import DetectionApi
|
from frigate.detectors.detection_api import DetectionApi
|
||||||
from frigate.detectors.detection_runners import OpenVINOModelRunner
|
from frigate.detectors.detection_runners import OpenVINOModelRunner
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user