mirror of
https://github.com/blakeblackshear/frigate.git
synced 2026-05-03 06:50:58 +00:00
Compare commits
7 Commits
61d3b370b1
...
cd519ed1ad
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cd519ed1ad | ||
|
|
2a860bd85e | ||
|
|
a7bbca5014 | ||
|
|
dc96940eb9 | ||
|
|
1408abb050 | ||
|
|
9c5e560668 | ||
|
|
b8fd0a2b31 |
2
.github/workflows/ci.yml
vendored
2
.github/workflows/ci.yml
vendored
@ -187,7 +187,7 @@ jobs:
|
||||
with:
|
||||
string: ${{ github.repository }}
|
||||
- name: Log in to the Container registry
|
||||
uses: docker/login-action@9780b0c442fbb1117ed29e0efdff1e18412f7567
|
||||
uses: docker/login-action@184bdaa0721073962dff0199f1fb9940f07167d1
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
|
||||
2
.github/workflows/release.yml
vendored
2
.github/workflows/release.yml
vendored
@ -18,7 +18,7 @@ jobs:
|
||||
with:
|
||||
string: ${{ github.repository }}
|
||||
- name: Log in to the Container registry
|
||||
uses: docker/login-action@9780b0c442fbb1117ed29e0efdff1e18412f7567
|
||||
uses: docker/login-action@184bdaa0721073962dff0199f1fb9940f07167d1
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
|
||||
@ -22,7 +22,7 @@ paho-mqtt == 2.1.*
|
||||
pandas == 2.2.*
|
||||
peewee == 3.17.*
|
||||
peewee_migrate == 1.13.*
|
||||
psutil == 6.1.*
|
||||
psutil == 7.1.*
|
||||
pydantic == 2.10.*
|
||||
git+https://github.com/fbcotter/py3nvml#egg=py3nvml
|
||||
pytz == 2025.*
|
||||
@ -40,7 +40,7 @@ titlecase == 2.4.*
|
||||
numpy == 1.26.*
|
||||
opencv-python-headless == 4.11.0.*
|
||||
opencv-contrib-python == 4.11.0.*
|
||||
scipy == 1.14.*
|
||||
scipy == 1.16.*
|
||||
# OpenVino & ONNX
|
||||
openvino == 2025.3.*
|
||||
onnxruntime == 1.22.*
|
||||
|
||||
@ -1,3 +1,2 @@
|
||||
onnx == 1.14.0; platform_machine == 'aarch64'
|
||||
protobuf == 3.20.3; platform_machine == 'aarch64'
|
||||
numpy == 1.23.*; platform_machine == 'aarch64' # required by python-tensorrt 8.2.1 (Jetpack 4.6)
|
||||
|
||||
@ -81,7 +81,7 @@ python3 -c 'import secrets; print(secrets.token_hex(64))'
|
||||
Frigate looks for a JWT token secret in the following order:
|
||||
|
||||
1. An environment variable named `FRIGATE_JWT_SECRET`
|
||||
2. A docker secret named `FRIGATE_JWT_SECRET` in `/run/secrets/`
|
||||
2. A file named `FRIGATE_JWT_SECRET` in the directory specified by the `CREDENTIALS_DIRECTORY` environment variable (defaults to the Docker Secrets directory: `/run/secrets/`)
|
||||
3. A `jwt_secret` option from the Home Assistant Add-on options
|
||||
4. A `.jwt_secret` file in the config directory
|
||||
|
||||
|
||||
@ -85,13 +85,13 @@ semantic_search:
|
||||
enabled: True
|
||||
model_size: large
|
||||
# Optional, if using the 'large' model in a multi-GPU installation
|
||||
device: 0
|
||||
device: 0
|
||||
```
|
||||
|
||||
:::info
|
||||
|
||||
If the correct build is used for your GPU / NPU and the `large` model is configured, then the GPU / NPU will be detected and used automatically.
|
||||
Specify the `device` option to target a specific GPU in a multi-GPU system (see [onnxruntime's provider options](https://onnxruntime.ai/docs/execution-providers/)).
|
||||
If the correct build is used for your GPU / NPU and the `large` model is configured, then the GPU / NPU will be detected and used automatically.
|
||||
Specify the `device` option to target a specific GPU in a multi-GPU system (see [onnxruntime's provider options](https://onnxruntime.ai/docs/execution-providers/)).
|
||||
If you do not specify a device, the first available GPU will be used.
|
||||
|
||||
See the [Hardware Accelerated Enrichments](/configuration/hardware_acceleration_enrichments.md) documentation.
|
||||
@ -144,3 +144,11 @@ When a trigger fires, the UI highlights the trigger with a blue outline for 3 se
|
||||
- Triggers rely on the same Jina AI CLIP models (V1 or V2) used for semantic search. Ensure `semantic_search` is enabled and properly configured.
|
||||
- Reindexing embeddings (via the UI or `reindex: True`) does not affect trigger configurations but may update the embeddings used for matching.
|
||||
- For optimal performance, use a system with sufficient RAM (8GB minimum, 16GB recommended) and a GPU for `large` model configurations, as described in the Semantic Search requirements.
|
||||
|
||||
### FAQ
|
||||
|
||||
#### Why can't I create a trigger on thumbnails for some text, like "person with a blue shirt" and have it trigger when a person with a blue shirt is detected?
|
||||
|
||||
TL;DR: Text-to-image triggers aren’t supported because CLIP can confuse similar images and give inconsistent scores, making automation unreliable.
|
||||
|
||||
Text-to-image triggers are not supported due to fundamental limitations of CLIP-based similarity search. While CLIP works well for exploratory, manual queries, it is unreliable for automated triggers based on a threshold. Issues include embedding drift (the same text–image pair can yield different cosine distances over time), lack of true semantic grounding (visually similar but incorrect matches), and unstable thresholding (distance distributions are dataset-dependent and often too tightly clustered to separate relevant from irrelevant results). Instead, it is recommended to set up a workflow with thumbnail triggers: first use text search to manually select 3–5 representative reference tracked objects, then configure thumbnail triggers based on that visual similarity. This provides robust automation without the semantic ambiguity of text to image matching.
|
||||
|
||||
@ -175,14 +175,17 @@ There are improved capabilities in newer GPU architectures that TensorRT can ben
|
||||
[NVIDIA GPU Compute Capability](https://developer.nvidia.com/cuda-gpus)
|
||||
|
||||
Inference speeds will vary greatly depending on the GPU and the model used.
|
||||
`tiny` variants are faster than the equivalent non-tiny model, some known examples are below:
|
||||
`tiny (t)` variants are faster than the equivalent non-tiny model, some known examples are below:
|
||||
|
||||
| Name | YOLOv9 Inference Time | YOLO-NAS Inference Time | RF-DETR Inference Time |
|
||||
| --------------- | --------------------- | ------------------------- | ---------------------- |
|
||||
| RTX 3050 | t-320: 15 ms | 320: ~ 10 ms 640: ~ 16 ms | Nano-320: ~ 12 ms |
|
||||
| RTX 3070 | t-320: 11 ms | 320: ~ 8 ms 640: ~ 14 ms | Nano-320: ~ 9 ms |
|
||||
| RTX A4000 | | 320: ~ 15 ms | |
|
||||
| Tesla P40 | | 320: ~ 105 ms | |
|
||||
✅ - Accelerated with CUDA Graphs
|
||||
❌ - Not accelerated with CUDA Graphs
|
||||
|
||||
| Name | ✅ YOLOv9 Inference Time | ✅ RF-DETR Inference Time | ❌ YOLO-NAS Inference Time
|
||||
| --------------- | ------------------------ | ------------------------- | -------------------------- |
|
||||
| RTX 3050 | t-320: 8 ms s-320: 10 ms | Nano-320: ~ 12 ms | 320: ~ 10 ms 640: ~ 16 ms |
|
||||
| RTX 3070 | t-320: 6 ms s-320: 8 ms | Nano-320: ~ 9 ms | 320: ~ 8 ms 640: ~ 14 ms |
|
||||
| RTX A4000 | | | 320: ~ 15 ms |
|
||||
| Tesla P40 | | | 320: ~ 105 ms |
|
||||
|
||||
### Apple Silicon
|
||||
|
||||
@ -203,9 +206,9 @@ Apple Silicon can not run within a container, so a ZMQ proxy is utilized to comm
|
||||
|
||||
With the [ROCm](../configuration/object_detectors.md#amdrocm-gpu-detector) detector Frigate can take advantage of many discrete AMD GPUs.
|
||||
|
||||
| Name | YOLOv9 Inference Time | YOLO-NAS Inference Time |
|
||||
| --------- | --------------------- | ------------------------- |
|
||||
| AMD 780M | ~ 14 ms | 320: ~ 25 ms 640: ~ 50 ms |
|
||||
| Name | YOLOv9 Inference Time | YOLO-NAS Inference Time |
|
||||
| --------- | ------------------------- | ------------------------- |
|
||||
| AMD 780M | t-320: 14 ms s-320: 20 ms | 320: ~ 25 ms 640: ~ 50 ms |
|
||||
|
||||
## Community Supported Detectors
|
||||
|
||||
|
||||
3073
docs/package-lock.json
generated
3073
docs/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -11,7 +11,7 @@ from datetime import datetime, timedelta
|
||||
from functools import reduce
|
||||
from io import StringIO
|
||||
from pathlib import Path as FilePath
|
||||
from typing import Any, Optional
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
||||
import aiofiles
|
||||
import requests
|
||||
@ -21,7 +21,7 @@ from fastapi.encoders import jsonable_encoder
|
||||
from fastapi.params import Depends
|
||||
from fastapi.responses import JSONResponse, PlainTextResponse, StreamingResponse
|
||||
from markupsafe import escape
|
||||
from peewee import SQL, operator
|
||||
from peewee import SQL, fn, operator
|
||||
from pydantic import ValidationError
|
||||
|
||||
from frigate.api.auth import require_role
|
||||
@ -130,7 +130,14 @@ def metrics(request: Request):
|
||||
"""Expose Prometheus metrics endpoint and update metrics with latest stats"""
|
||||
# Retrieve the latest statistics and update the Prometheus metrics
|
||||
stats = request.app.stats_emitter.get_latest_stats()
|
||||
update_metrics(stats)
|
||||
# query DB for count of events by camera, label
|
||||
event_counts: List[Dict[str, Any]] = (
|
||||
Event.select(Event.camera, Event.label, fn.Count())
|
||||
.group_by(Event.camera, Event.label)
|
||||
.dicts()
|
||||
)
|
||||
|
||||
update_metrics(stats=stats, event_counts=event_counts)
|
||||
content, content_type = get_metrics()
|
||||
return Response(content=content, media_type=content_type)
|
||||
|
||||
|
||||
@ -5,12 +5,13 @@ from typing import Annotated
|
||||
from pydantic import AfterValidator, ValidationInfo
|
||||
|
||||
FRIGATE_ENV_VARS = {k: v for k, v in os.environ.items() if k.startswith("FRIGATE_")}
|
||||
# read docker secret files as env vars too
|
||||
if os.path.isdir("/run/secrets") and os.access("/run/secrets", os.R_OK):
|
||||
for secret_file in os.listdir("/run/secrets"):
|
||||
secrets_dir = os.environ.get("CREDENTIALS_DIRECTORY", "/run/secrets")
|
||||
# read secret files as env vars too
|
||||
if os.path.isdir(secrets_dir) and os.access(secrets_dir, os.R_OK):
|
||||
for secret_file in os.listdir(secrets_dir):
|
||||
if secret_file.startswith("FRIGATE_"):
|
||||
FRIGATE_ENV_VARS[secret_file] = (
|
||||
Path(os.path.join("/run/secrets", secret_file)).read_text().strip()
|
||||
Path(os.path.join(secrets_dir, secret_file)).read_text().strip()
|
||||
)
|
||||
|
||||
|
||||
|
||||
@ -420,16 +420,27 @@ def get_optimized_runner(
|
||||
if device != "CPU" and is_openvino_gpu_npu_available():
|
||||
return OpenVINOModelRunner(model_path, device, model_type, **kwargs)
|
||||
|
||||
ortSession = ort.InferenceSession(
|
||||
model_path,
|
||||
providers=providers,
|
||||
provider_options=options,
|
||||
)
|
||||
|
||||
if (
|
||||
not CudaGraphRunner.is_complex_model(model_type)
|
||||
and providers[0] == "CUDAExecutionProvider"
|
||||
):
|
||||
return CudaGraphRunner(ortSession, options[0]["device_id"])
|
||||
options[0] = {
|
||||
**options[0],
|
||||
"enable_cuda_graph": True,
|
||||
}
|
||||
return CudaGraphRunner(
|
||||
ort.InferenceSession(
|
||||
model_path,
|
||||
providers=providers,
|
||||
provider_options=options,
|
||||
),
|
||||
options[0]["device_id"],
|
||||
)
|
||||
|
||||
return ONNXModelRunner(ortSession)
|
||||
return ONNXModelRunner(
|
||||
ort.InferenceSession(
|
||||
model_path,
|
||||
providers=providers,
|
||||
provider_options=options,
|
||||
)
|
||||
)
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
import logging
|
||||
import re
|
||||
from typing import Any, Dict, List
|
||||
|
||||
from prometheus_client import CONTENT_TYPE_LATEST, generate_latest
|
||||
from prometheus_client.core import (
|
||||
@ -450,51 +451,17 @@ class CustomCollector(object):
|
||||
yield storage_total
|
||||
yield storage_used
|
||||
|
||||
# count events
|
||||
events = []
|
||||
|
||||
if len(events) > 0:
|
||||
# events[0] is newest event, last element is oldest, don't need to sort
|
||||
|
||||
if not self.previous_event_id:
|
||||
# ignore all previous events on startup, prometheus might have already counted them
|
||||
self.previous_event_id = events[0]["id"]
|
||||
self.previous_event_start_time = int(events[0]["start_time"])
|
||||
|
||||
for event in events:
|
||||
# break if event already counted
|
||||
if event["id"] == self.previous_event_id:
|
||||
break
|
||||
|
||||
# break if event starts before previous event
|
||||
if event["start_time"] < self.previous_event_start_time:
|
||||
break
|
||||
|
||||
# store counted events in a dict
|
||||
try:
|
||||
cam = self.all_events[event["camera"]]
|
||||
try:
|
||||
cam[event["label"]] += 1
|
||||
except KeyError:
|
||||
# create label dict if not exists
|
||||
cam.update({event["label"]: 1})
|
||||
except KeyError:
|
||||
# create camera and label dict if not exists
|
||||
self.all_events.update({event["camera"]: {event["label"]: 1}})
|
||||
|
||||
# don't recount events next time
|
||||
self.previous_event_id = events[0]["id"]
|
||||
self.previous_event_start_time = int(events[0]["start_time"])
|
||||
|
||||
camera_events = CounterMetricFamily(
|
||||
"frigate_camera_events",
|
||||
"Count of camera events since exporter started",
|
||||
labels=["camera", "label"],
|
||||
)
|
||||
|
||||
for camera, cam_dict in self.all_events.items():
|
||||
for label, label_value in cam_dict.items():
|
||||
camera_events.add_metric([camera, label], label_value)
|
||||
if len(self.all_events) > 0:
|
||||
for event_count in self.all_events:
|
||||
camera_events.add_metric(
|
||||
[event_count["camera"], event_count["label"]], event_count["Count"]
|
||||
)
|
||||
|
||||
yield camera_events
|
||||
|
||||
@ -503,7 +470,7 @@ collector = CustomCollector(None)
|
||||
REGISTRY.register(collector)
|
||||
|
||||
|
||||
def update_metrics(stats):
|
||||
def update_metrics(stats: Dict[str, Any], event_counts: List[Dict[str, Any]]):
|
||||
"""Updates the Prometheus metrics with the given stats data."""
|
||||
try:
|
||||
# Store the complete stats for later use by collect()
|
||||
@ -512,6 +479,8 @@ def update_metrics(stats):
|
||||
# For backwards compatibility
|
||||
collector.process_stats = stats.copy()
|
||||
|
||||
collector.all_events = event_counts
|
||||
|
||||
# No need to call collect() here - it will be called by get_metrics()
|
||||
except Exception as e:
|
||||
logging.error(f"Error updating metrics: {e}")
|
||||
|
||||
@ -8,7 +8,9 @@ from playhouse.shortcuts import model_to_dict
|
||||
from frigate.api.auth import get_allowed_cameras_for_filter, get_current_user
|
||||
from frigate.comms.event_metadata_updater import EventMetadataPublisher
|
||||
from frigate.models import Event, Recordings, ReviewSegment, Timeline
|
||||
from frigate.stats.emitter import StatsEmitter
|
||||
from frigate.test.http_api.base_http_test import BaseTestHttp
|
||||
from frigate.test.test_storage import _insert_mock_event
|
||||
|
||||
|
||||
class TestHttpApp(BaseTestHttp):
|
||||
@ -293,3 +295,108 @@ class TestHttpApp(BaseTestHttp):
|
||||
sub_labels = client.get("/sub_labels").json()
|
||||
assert sub_labels
|
||||
assert sub_labels == [sub_label]
|
||||
|
||||
####################################################################################################################
|
||||
################################### GET /metrics Endpoint #########################################################
|
||||
####################################################################################################################
|
||||
def test_get_metrics(self):
|
||||
"""ensure correct prometheus metrics api response"""
|
||||
with TestClient(self.app) as client:
|
||||
ts_start = datetime.now().timestamp()
|
||||
ts_end = ts_start + 30
|
||||
_insert_mock_event(
|
||||
id="abcde.random", start=ts_start, end=ts_end, retain=True
|
||||
)
|
||||
_insert_mock_event(
|
||||
id="01234.random", start=ts_start, end=ts_end, retain=True
|
||||
)
|
||||
_insert_mock_event(
|
||||
id="56789.random", start=ts_start, end=ts_end, retain=True
|
||||
)
|
||||
_insert_mock_event(
|
||||
id="101112.random",
|
||||
label="outside",
|
||||
start=ts_start,
|
||||
end=ts_end,
|
||||
retain=True,
|
||||
)
|
||||
_insert_mock_event(
|
||||
id="131415.random",
|
||||
label="outside",
|
||||
start=ts_start,
|
||||
end=ts_end,
|
||||
retain=True,
|
||||
)
|
||||
_insert_mock_event(
|
||||
id="161718.random",
|
||||
camera="porch",
|
||||
start=ts_start,
|
||||
end=ts_end,
|
||||
retain=True,
|
||||
)
|
||||
_insert_mock_event(
|
||||
id="192021.random",
|
||||
camera="porch",
|
||||
start=ts_start,
|
||||
end=ts_end,
|
||||
retain=True,
|
||||
)
|
||||
_insert_mock_event(
|
||||
id="222324.random",
|
||||
camera="porch",
|
||||
label="inside",
|
||||
start=ts_start,
|
||||
end=ts_end,
|
||||
retain=True,
|
||||
)
|
||||
_insert_mock_event(
|
||||
id="252627.random",
|
||||
camera="porch",
|
||||
label="inside",
|
||||
start=ts_start,
|
||||
end=ts_end,
|
||||
retain=True,
|
||||
)
|
||||
_insert_mock_event(
|
||||
id="282930.random",
|
||||
label="inside",
|
||||
start=ts_start,
|
||||
end=ts_end,
|
||||
retain=True,
|
||||
)
|
||||
_insert_mock_event(
|
||||
id="313233.random",
|
||||
label="inside",
|
||||
start=ts_start,
|
||||
end=ts_end,
|
||||
retain=True,
|
||||
)
|
||||
|
||||
stats_emitter = Mock(spec=StatsEmitter)
|
||||
stats_emitter.get_latest_stats.return_value = self.test_stats
|
||||
self.app.stats_emitter = stats_emitter
|
||||
event = client.get("/metrics")
|
||||
|
||||
assert "# TYPE frigate_detection_total_fps gauge" in event.text
|
||||
assert "frigate_detection_total_fps 13.7" in event.text
|
||||
assert (
|
||||
"# HELP frigate_camera_events_total Count of camera events since exporter started"
|
||||
in event.text
|
||||
)
|
||||
assert "# TYPE frigate_camera_events_total counter" in event.text
|
||||
assert (
|
||||
'frigate_camera_events_total{camera="front_door",label="Mock"} 3.0'
|
||||
in event.text
|
||||
)
|
||||
assert (
|
||||
'frigate_camera_events_total{camera="front_door",label="inside"} 2.0'
|
||||
in event.text
|
||||
)
|
||||
assert (
|
||||
'frigate_camera_events_total{camera="front_door",label="outside"} 2.0'
|
||||
in event.text
|
||||
)
|
||||
assert (
|
||||
'frigate_camera_events_total{camera="porch",label="Mock"} 2.0' in event.text
|
||||
)
|
||||
assert 'frigate_camera_events_total{camera="porch",label="inside"} 2.0'
|
||||
|
||||
@ -261,12 +261,19 @@ class TestHttp(unittest.TestCase):
|
||||
assert Recordings.get(Recordings.id == rec_k3_id)
|
||||
|
||||
|
||||
def _insert_mock_event(id: str, start: int, end: int, retain: bool) -> Event:
|
||||
def _insert_mock_event(
|
||||
id: str,
|
||||
start: int,
|
||||
end: int,
|
||||
retain: bool,
|
||||
camera: str = "front_door",
|
||||
label: str = "Mock",
|
||||
) -> Event:
|
||||
"""Inserts a basic event model with a given id."""
|
||||
return Event.insert(
|
||||
id=id,
|
||||
label="Mock",
|
||||
camera="front_door",
|
||||
label=label,
|
||||
camera=camera,
|
||||
start_time=start,
|
||||
end_time=end,
|
||||
top_score=100,
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user