Compare commits

..
Author SHA1 Message Date
dependabot[bot]andGitHub 5c9295ffbc Bump postcss-selector-parser, tailwind-scrollbar and tailwindcss in /web
Removes [postcss-selector-parser](https://github.com/postcss/postcss-selector-parser). It's no longer used after updating ancestor dependencies [postcss-selector-parser](https://github.com/postcss/postcss-selector-parser), [tailwind-scrollbar](https://github.com/adoxography/tailwind-scrollbar) and [tailwindcss](https://github.com/tailwindlabs/tailwindcss/tree/HEAD/packages/tailwindcss). These dependencies need to be updated together.


Removes `postcss-selector-parser`

Updates `tailwind-scrollbar` from 3.1.0 to 4.0.2
- [Release notes](https://github.com/adoxography/tailwind-scrollbar/releases)
- [Commits](https://github.com/adoxography/tailwind-scrollbar/compare/v3.1.0...v4.0.2)

Updates `tailwindcss` from 3.4.19 to 4.3.3
- [Release notes](https://github.com/tailwindlabs/tailwindcss/releases)
- [Changelog](https://github.com/tailwindlabs/tailwindcss/blob/main/CHANGELOG.md)
- [Commits](https://github.com/tailwindlabs/tailwindcss/commits/v4.3.3/packages/tailwindcss)

---
updated-dependencies:
- dependency-name: postcss-selector-parser
  dependency-version:
  dependency-type: indirect
- dependency-name: tailwind-scrollbar
  dependency-version: 4.0.2
  dependency-type: direct:production
- dependency-name: tailwindcss
  dependency-version: 4.3.3
  dependency-type: direct:development
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-10-10 20:04:49 +00:00
11 changed files with 146 additions and 1039 deletions
+4 -4
View File
@@ -34,15 +34,15 @@ runs:
run: sudo systemctl start docker
shell: bash
- id: lowercaseRepo
uses: ASzc/change-string-case-action@v8
uses: ASzc/change-string-case-action@v5
with:
string: ${{ github.repository }}
- name: Set up QEMU
uses: docker/setup-qemu-action@v4
uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v4
uses: docker/setup-buildx-action@v3
- name: Log in to the Container registry
uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f
uses: docker/login-action@465a07811f14bebb1938fbed4728c6a1ff8901fc
with:
registry: ghcr.io
username: ${{ github.actor }}
+22 -139
View File
@@ -1,157 +1,40 @@
version: 2
updates:
- package-ecosystem: "github-actions"
# Local composite actions are only scanned when their directory is listed
directories:
- "/"
- "/.github/actions/setup"
directory: "/"
schedule:
interval: weekly
cooldown:
default-days: 3
open-pull-requests-limit: 5
groups:
actions:
patterns: ["*"]
actions-security:
applies-to: security-updates
patterns: ["*"]
interval: daily
open-pull-requests-limit: 10
target-branch: dev
- package-ecosystem: "docker"
directory: "/docker"
schedule:
interval: daily
open-pull-requests-limit: 10
target-branch: dev
- package-ecosystem: "pip"
directory: "/docker/main"
schedule:
interval: weekly
cooldown:
default-days: 3
open-pull-requests-limit: 5
groups:
# Sets that fail pip resolution unless they move together
peewee:
patterns: ["peewee", "peewee-migrate"]
crypto:
patterns: ["cryptography", "joserfc", "pywebpush", "py-vapid"]
pydantic-genai:
patterns: ["pydantic", "google-genai"]
pip-main-minor-patch:
patterns: ["*"]
update-types: ["minor", "patch"]
pip-main-security:
applies-to: security-updates
patterns: ["*"]
ignore:
# numpy 2.5 needs Python 3.12; norfair, tflite_runtime and rknn-toolkit2
# cap numpy below 2
- dependency-name: "numpy"
versions: [">=2"]
# Every OpenCV wheel from 4.12 requires numpy 2
- dependency-name: "opencv-python-headless"
versions: [">=4.12"]
- dependency-name: "opencv-contrib-python"
versions: [">=4.12"]
# aarch64 only. Rockchip rknn-toolkit2 caps protobuf at 4.25.4 and
# TensorFlow 2.21 needs protobuf 6.31 or newer.
- dependency-name: "tensorflow"
versions: [">=2.20"]
# Needs Python 3.12 and numpy 2
- dependency-name: "librosa"
versions: [">=1.0"]
# Needs Python 3.12
- dependency-name: "scipy"
versions: [">=1.18"]
# Newer stubs report false errors against peewee 4
- dependency-name: "types-peewee"
versions: [">=4.1"]
# sdist only and imported by HailoRT; needs a Hailo device to test
- dependency-name: "netifaces"
versions: [">=0.11"]
interval: daily
open-pull-requests-limit: 10
target-branch: dev
- package-ecosystem: "pip"
directory: "/docker/tensorrt"
schedule:
interval: weekly
# No pull request check builds these images, so version updates are off.
# Security updates still open.
open-pull-requests-limit: 0
groups:
pip-trt-security:
applies-to: security-updates
patterns: ["*"]
ignore:
# onnx 1.17 and newer write a newer IR and opset than TensorRT 8.6 on
# JetPack 6 is known to parse
- dependency-name: "onnx"
versions: [">=1.17"]
# 1.27 moved to CUDA 13, and the image ships the CUDA 12.8 wheel set
- dependency-name: "onnxruntime-gpu"
versions: [">=1.27"]
# Hand-matched CUDA 12.8 set (2c9a25e67); move as a block
- dependency-name: "nvidia-*"
# Must match the JetPack CUDA runtime
- dependency-name: "cuda-python"
versions: [">=12.9"]
interval: daily
open-pull-requests-limit: 10
target-branch: dev
- package-ecosystem: "npm"
directory: "/web"
schedule:
interval: weekly
cooldown:
default-days: 3
open-pull-requests-limit: 5
groups:
radix:
patterns: ["@radix-ui/*"]
rjsf:
patterns: ["@rjsf/*"]
web-minor-patch:
patterns: ["*"]
update-types: ["minor", "patch"]
web-security:
applies-to: security-updates
patterns: ["*"]
ignore:
# typescript-eslint peers typescript below 6.1
- dependency-name: "typescript"
versions: [">=6.1"]
# Tailwind 4 is a config rewrite; scrollbar 4 and merge 3 depend on it
- dependency-name: "tailwindcss"
update-types: ["version-update:semver-major"]
- dependency-name: "tailwind-scrollbar"
update-types: ["version-update:semver-major"]
- dependency-name: "tailwind-merge"
update-types: ["version-update:semver-major"]
# Intermittent failure in the UI settings import spec, cause not found
- dependency-name: "zod"
update-types: ["version-update:semver-major"]
# Needs an on-device pinch test before moving
- dependency-name: "react-zoom-pan-pinch"
update-types: ["version-update:semver-major"]
# Build fails until monaco-worker-manager releases its PR 4
- dependency-name: "monaco-editor"
versions: [">=0.55.0"]
# Later versions need Node 22 and extract false keys
- dependency-name: "i18next-cli"
versions: [">1.5.11"]
# 6.11.0 refills the saved model in the GenAI settings form
- dependency-name: "@rjsf/*"
versions: ["6.11.0"]
interval: daily
open-pull-requests-limit: 10
target-branch: dev
- package-ecosystem: "npm"
directory: "/docs"
schedule:
interval: weekly
cooldown:
default-days: 3
open-pull-requests-limit: 5
interval: daily
allow:
- dependency-name: "@docusaurus/*"
groups:
docusaurus:
patterns: ["@docusaurus/*"]
docs-security:
applies-to: security-updates
patterns: ["*"]
ignore:
# A Docusaurus major needs the openapi plugin and theme moved in the
# same commit
- dependency-name: "@docusaurus/*"
update-types: ["version-update:semver-major"]
open-pull-requests-limit: 10
target-branch: dev
+1 -1
View File
@@ -586,7 +586,7 @@ jobs:
- arm64_build
steps:
- id: lowercaseRepo
uses: ASzc/change-string-case-action@v8
uses: ASzc/change-string-case-action@v6
with:
string: ${{ github.repository }}
- name: Log in to the Container registry
+1 -1
View File
@@ -14,7 +14,7 @@ jobs:
with:
persist-credentials: false
- id: lowercaseRepo
uses: ASzc/change-string-case-action@v8
uses: ASzc/change-string-case-action@v6
with:
string: ${{ github.repository }}
- name: Log in to the Container registry
-41
View File
@@ -1,41 +0,0 @@
import unittest
import cv2
import numpy as np
from frigate.util.image import get_histogram
def reference_histogram(image, x_min, y_min, x_max, y_max):
bgr = cv2.cvtColor(image, cv2.COLOR_YUV2BGR_I420)[y_min:y_max, x_min:x_max]
hist = cv2.calcHist([bgr], [0, 1, 2], None, [8, 8, 8], [0, 256, 0, 256, 0, 256])
return cv2.normalize(hist, hist).flatten()
class TestGetHistogram(unittest.TestCase):
def setUp(self):
rng = np.random.default_rng(0)
self.frame = rng.integers(0, 255, (720 * 3 // 2, 1280), np.uint8)
def test_matches_full_frame_conversion_on_even_box(self):
box = (100, 200, 400, 600)
np.testing.assert_array_equal(
get_histogram(self.frame, *box), reference_histogram(self.frame, *box)
)
def test_odd_box_widens_to_even_edges(self):
np.testing.assert_array_equal(
get_histogram(self.frame, 101, 201, 399, 599),
reference_histogram(self.frame, 100, 200, 400, 600),
)
def test_box_is_clamped_to_frame(self):
np.testing.assert_array_equal(
get_histogram(self.frame, -10, -10, 5000, 5000),
reference_histogram(self.frame, 0, 0, 1280, 720),
)
def test_empty_box_returns_zeros(self):
hist = get_histogram(self.frame, 50, 50, 50, 50)
self.assertEqual(hist.shape, (512,))
self.assertEqual(hist.sum(), 0)
-136
View File
@@ -1,136 +0,0 @@
"""Tracker selection when autotracking config changes at runtime."""
import unittest
from unittest.mock import MagicMock
import numpy as np
from frigate.camera import PTZMetrics
from frigate.config import FrigateConfig
from frigate.track.norfair_tracker import NorfairTracker
CAMERA = "ptz_cam"
BOX = (400, 200, 500, 500)
def _config(enabled: bool, track: list[str] | None = None) -> FrigateConfig:
autotracking: dict = {"enabled": enabled, "required_zones": ["zone"]}
if track is not None:
autotracking["track"] = track
return FrigateConfig(
**{
"mqtt": {"enabled": False},
"cameras": {
CAMERA: {
"ffmpeg": {
"inputs": [
{"path": "rtsp://10.0.0.1:554/video", "roles": ["detect"]}
]
},
"detect": {"width": 1280, "height": 720},
"zones": {"zone": {"coordinates": "0,0,1,0,1,1,0,1"}},
"onvif": {"host": "10.0.0.1", "autotracking": autotracking},
}
},
}
)
class TestTrackerSelection(unittest.TestCase):
def setUp(self) -> None:
self.frame_time = 1000.0
def make_tracker(self, enabled: bool) -> NorfairTracker:
camera_config = _config(enabled).cameras[CAMERA]
tracker = NorfairTracker(camera_config, PTZMetrics())
tracker.frame_manager = MagicMock()
tracker.frame_manager.get.return_value = np.zeros(
camera_config.frame_shape_yuv, dtype=np.uint8
)
tracker.ptz_motion_estimator = MagicMock()
tracker.ptz_motion_estimator.motion_estimator.return_value = None
return tracker
def apply_onvif_update(self, tracker: NorfairTracker, config: FrigateConfig):
"""Apply an onvif config update the way the camera process does."""
tracker.camera_config.onvif = config.cameras[CAMERA].onvif
tracker.sync_trackers()
def run_frames(self, tracker: NorfairTracker, count: int, label="person"):
for _ in range(count):
self.frame_time += 0.2
tracker.match_and_update(
"frame",
self.frame_time,
[(label, 0.9, BOX, 30000, 0.33, (0, 0, 640, 640))],
)
def test_disabling_autotracking_falls_back_to_static_tracker(self):
tracker = self.make_tracker(enabled=True)
self.run_frames(tracker, 10)
self.assertIs(tracker.get_tracker("person"), tracker.trackers["person"]["ptz"])
self.apply_onvif_update(tracker, _config(enabled=False))
self.run_frames(tracker, 10)
self.assertIs(tracker.get_tracker("person"), tracker.default_tracker["static"])
self.assertEqual(len(tracker.tracked_objects), 1)
def test_enabling_autotracking_uses_ptz_tracker(self):
tracker = self.make_tracker(enabled=False)
self.run_frames(tracker, 10)
self.assertIs(tracker.get_tracker("person"), tracker.default_tracker["static"])
self.apply_onvif_update(tracker, _config(enabled=True))
self.run_frames(tracker, 10)
self.assertIs(tracker.get_tracker("person"), tracker.trackers["person"]["ptz"])
self.assertEqual(len(tracker.tracked_objects), 1)
def test_removing_label_from_autotracking_keeps_tracking(self):
tracker = self.make_tracker(enabled=True)
self.run_frames(tracker, 10)
self.apply_onvif_update(tracker, _config(enabled=True, track=["car"]))
self.run_frames(tracker, 10)
self.assertNotIn("person", tracker.trackers)
self.assertIs(tracker.get_tracker("person"), tracker.default_tracker["ptz"])
self.assertEqual(len(tracker.tracked_objects), 1)
def test_unrelated_onvif_update_keeps_objects(self):
tracker = self.make_tracker(enabled=True)
self.run_frames(tracker, 10)
ids = set(tracker.tracked_objects)
config = _config(enabled=True)
config.cameras[CAMERA].onvif.password = "changed"
self.apply_onvif_update(tracker, config)
self.run_frames(tracker, 5)
self.assertEqual(set(tracker.tracked_objects), ids)
def test_runtime_toggle_keeps_objects(self):
# MQTT and the autotracker only flip enabled, never enabled_in_config
tracker = self.make_tracker(enabled=True)
self.run_frames(tracker, 10)
ids = set(tracker.tracked_objects)
tracker.camera_config.onvif.autotracking.enabled = False
tracker.sync_trackers()
self.run_frames(tracker, 5)
self.assertEqual(set(tracker.tracked_objects), ids)
def test_unlisted_label_uses_the_default_tracker_that_holds_it(self):
tracker = self.make_tracker(enabled=True)
self.run_frames(tracker, 10, label="dog")
default = tracker.get_tracker("dog")
self.assertIs(default, tracker.default_tracker["ptz"])
self.assertEqual(
{str(o.global_id) for o in default.tracked_objects},
set(tracker.track_id_map),
)
+57 -81
View File
@@ -180,9 +180,48 @@ class NorfairTracker(ObjectTracker):
}
self.trackers: dict[str, dict[str, Tracker]] = {}
self.default_tracker: dict[str, Tracker] = {}
self.tracker_selection: tuple[bool | None, tuple[str, ...]] | None = None
self.sync_trackers()
# Handle static trackers
for obj_type, tracker_config in self.object_type_configs.items():
if obj_type in self.camera_config.objects.track:
if obj_type not in self.trackers:
self.trackers[obj_type] = {}
self.trackers[obj_type]["static"] = self._create_tracker(
obj_type, tracker_config
)
# Handle PTZ trackers
for obj_type, tracker_config in self.ptz_object_type_configs.items():
if (
obj_type in self.camera_config.onvif.autotracking.track
and self.camera_config.onvif.autotracking.enabled_in_config
):
if obj_type not in self.trackers:
self.trackers[obj_type] = {}
self.trackers[obj_type]["ptz"] = self._create_tracker(
obj_type, tracker_config
)
# Initialize default trackers
self.default_tracker = {
"static": Tracker(
distance_function=frigate_distance,
distance_threshold=self.default_tracker_config[ # type: ignore[arg-type]
"distance_threshold"
],
initialization_delay=self.detect_config.min_initialized,
hit_counter_max=self.detect_config.max_disappeared, # type: ignore[arg-type]
filter_factory=self.default_tracker_config["filter_factory"], # type: ignore[arg-type]
),
"ptz": Tracker(
distance_function=frigate_distance,
distance_threshold=self.default_ptz_tracker_config[
"distance_threshold"
], # type: ignore[arg-type]
initialization_delay=self.detect_config.min_initialized,
hit_counter_max=self.detect_config.max_disappeared, # type: ignore[arg-type]
filter_factory=self.default_ptz_tracker_config["filter_factory"], # type: ignore[arg-type]
),
}
if self.camera_config.onvif.autotracking.enabled:
self.ptz_motion_estimator = PtzMotionEstimator(
@@ -218,86 +257,18 @@ class NorfairTracker(ObjectTracker):
return Tracker(**tracker_params)
def _tracker_selection(self) -> tuple[bool | None, tuple[str, ...]]:
autotracking = self.camera_config.onvif.autotracking
ptz_labels = tuple(
label
for label in self.ptz_object_type_configs
if label in autotracking.track
)
return (autotracking.enabled_in_config, ptz_labels)
def sync_trackers(self) -> None:
"""Rebuild the trackers when the config that selects them has changed.
The camera process receives onvif config updates at runtime, so which
labels use a PTZ tracker can change after startup. Rebuilding drops
norfair state, so an update that leaves the selection alone is a no-op.
"""
selection = self._tracker_selection()
if selection == self.tracker_selection:
return
if self.tracker_selection is not None:
logger.debug(
"%s: autotracking changed, rebuilding trackers", self.camera_name
)
self.tracker_selection = selection
ptz_enabled, ptz_labels = selection
self.trackers = {}
for obj_type, tracker_config in self.object_type_configs.items():
if obj_type in self.camera_config.objects.track:
self.trackers.setdefault(obj_type, {})["static"] = self._create_tracker(
obj_type, tracker_config
)
if ptz_enabled:
for obj_type, tracker_config in self.ptz_object_type_configs.items():
if obj_type in ptz_labels:
self.trackers.setdefault(obj_type, {})["ptz"] = (
self._create_tracker(obj_type, tracker_config)
)
self.default_tracker = {
"static": Tracker(
distance_function=frigate_distance,
distance_threshold=self.default_tracker_config[ # type: ignore[arg-type]
"distance_threshold"
],
initialization_delay=self.detect_config.min_initialized,
hit_counter_max=self.detect_config.max_disappeared, # type: ignore[arg-type]
filter_factory=self.default_tracker_config["filter_factory"], # type: ignore[arg-type]
),
"ptz": Tracker(
distance_function=frigate_distance,
distance_threshold=self.default_ptz_tracker_config[
"distance_threshold"
], # type: ignore[arg-type]
initialization_delay=self.detect_config.min_initialized,
hit_counter_max=self.detect_config.max_disappeared, # type: ignore[arg-type]
filter_factory=self.default_ptz_tracker_config["filter_factory"], # type: ignore[arg-type]
),
}
def default_mode(self) -> str:
return (
def get_tracker(self, object_type: str) -> Tracker:
"""Get the appropriate tracker based on object type and camera mode."""
mode = (
"ptz"
if self.camera_config.onvif.autotracking.enabled_in_config
and object_type in self.camera_config.onvif.autotracking.track
and object_type in self.ptz_object_type_configs.keys()
else "static"
)
def get_tracker(self, object_type: str) -> Tracker:
"""Get the tracker that match_and_update feeds this label's detections to."""
trackers = self.trackers.get(object_type)
if trackers is None:
return self.default_tracker[self.default_mode()]
# sync_trackers only creates a ptz tracker for labels the config selects
return trackers["ptz"] if "ptz" in trackers else trackers["static"]
if object_type in self.trackers:
return self.trackers[object_type][mode]
return self.default_tracker[mode]
def register(self, track_id: str, obj: dict[str, Any]) -> None:
rand_id = "".join(random.choices(string.ascii_lowercase + string.digits, k=6))
@@ -563,7 +534,7 @@ class NorfairTracker(ObjectTracker):
points = np.array([[obj[2][0], obj[2][1]], [obj[2][2], obj[2][3]]])
embedding = None
if self.camera_config.onvif.autotracking.enabled and yuv_frame is not None:
if self.camera_config.onvif.autotracking.enabled:
embedding = get_histogram(
yuv_frame, obj[2][0], obj[2][1], obj[2][2], obj[2][3]
)
@@ -616,7 +587,12 @@ class NorfairTracker(ObjectTracker):
default_detections.extend(dets)
# Update default tracker with untracked detections
tracked_objects = self.default_tracker[self.default_mode()].update(
mode = (
"ptz"
if self.camera_config.onvif.autotracking.enabled_in_config
else "static"
)
tracked_objects = self.default_tracker[mode].update(
detections=default_detections, coord_transformations=coord_transformations
)
all_tracked_objects.extend(tracked_objects)
+3 -36
View File
@@ -1241,42 +1241,9 @@ def get_image_from_recording(
return image_data
def get_histogram(
image: np.ndarray, x_min: int, y_min: int, x_max: int, y_max: int
) -> np.ndarray:
"""Return a normalized 8x8x8 BGR histogram of a box in an I420 frame.
The box is cropped from each YUV plane before color conversion, so the
cost depends on the box size rather than the frame size. Box edges are
widened to even coordinates to keep chroma alignment.
"""
height = image.shape[0] * 2 // 3
width = image.shape[1]
x_min = max(0, x_min // 2 * 2)
y_min = max(0, y_min // 2 * 2)
x_max = min(width, (x_max + 1) // 2 * 2)
y_max = min(height, (y_max + 1) // 2 * 2)
if x_max - x_min < 2 or y_max - y_min < 2:
return np.zeros(512, np.float32)
flat = image.reshape(-1)
y_size = height * width
uv_size = y_size // 4
y_plane = flat[:y_size].reshape(height, width)
u_plane = flat[y_size : y_size + uv_size].reshape(height // 2, width // 2)
v_plane = flat[y_size + uv_size : y_size + 2 * uv_size].reshape(
height // 2, width // 2
)
crop = np.concatenate(
(
y_plane[y_min:y_max, x_min:x_max].ravel(),
u_plane[y_min // 2 : y_max // 2, x_min // 2 : x_max // 2].ravel(),
v_plane[y_min // 2 : y_max // 2, x_min // 2 : x_max // 2].ravel(),
)
).reshape((y_max - y_min) * 3 // 2, x_max - x_min)
image_bgr = cv2.cvtColor(crop, cv2.COLOR_YUV2BGR_I420)
def get_histogram(image, x_min, y_min, x_max, y_max):
image_bgr = cv2.cvtColor(image, cv2.COLOR_YUV2BGR_I420)
image_bgr = image_bgr[y_min:y_max, x_min:x_max]
hist = cv2.calcHist(
[image_bgr], [0, 1, 2], None, [8, 8, 8], [0, 256, 0, 256, 0, 256]
-1
View File
@@ -302,7 +302,6 @@ def process_frames(
motion_detector.autotracking_enabled = (
camera_config.onvif.autotracking.enabled
)
object_tracker.sync_trackers()
if (
not camera_enabled
+56 -597
View File
File diff suppressed because it is too large Load Diff
+2 -2
View File
@@ -89,7 +89,7 @@
"sonner": "^2.0.8",
"swr": "^2.5.1",
"tailwind-merge": "^2.4.0",
"tailwind-scrollbar": "^3.1.0",
"tailwind-scrollbar": "^4.0.2",
"tailwindcss-animate": "^1.0.7",
"use-long-press": "^3.3.0",
"vaul": "^1.1.2",
@@ -120,7 +120,7 @@
"postcss": "^8.5.29",
"prettier": "^3.9.9",
"prettier-plugin-tailwindcss": "^0.8.1",
"tailwindcss": "^3.4.19",
"tailwindcss": "^4.3.3",
"typescript": "^6.0.3",
"typescript-eslint": "^8.71.1",
"vite": "^8.3.3"