Compare commits

..
8 Commits
Author SHA1 Message Date
Josh Hawkins da891cfa66 remove duplicate language 2026-02-09 09:42:54 -06:00
Josh Hawkins 92318c79d5 prevent console warnings about missing titles and descriptions
make these invisible with sr-only
2026-02-09 09:42:40 -06:00
Josh Hawkins 1d506c1684 work around radix pointer events issue when dialog is opened from drawer
fixes https://github.com/blakeblackshear/frigate/discussions/21940
2026-02-09 09:42:11 -06:00
Josh Hawkins e21565a209 formatting 2026-02-08 07:15:49 -06:00
Josh Hawkins e658a70e0f clean up 2026-02-08 07:08:00 -06:00
Josh Hawkins 82cb69526b add admin precedence to proxy role_map resolution to prevent downgrade 2026-02-08 07:05:32 -06:00
Josh Hawkins fe3677c7df Add grace period for recording segment checks to prevent spurious ffmpeg restarts 2026-02-08 07:03:35 -06:00
Josh Hawkins 1fec95f88e improve chip tooltip display
- use formatList to use i18n separators instead of commas
- ensure the correct event type is used so sublabels are not run through normalization
- remove smart-capitalization classes as translated labels use i18n (which includes capitalization)
- give icons an optional key so that the console doesn't complain about duplication when rendering
2026-02-08 07:02:40 -06:00
10 changed files with 33 additions and 129 deletions
+16 -26
View File
@@ -69,25 +69,6 @@ logger = logging.getLogger(__name__)
router = APIRouter(tags=[Tags.events])
def _build_attribute_filter_clause(attributes: str):
filtered_attributes = [
attr.strip() for attr in attributes.split(",") if attr.strip()
]
attribute_clauses = []
for attr in filtered_attributes:
attribute_clauses.append(Event.data.cast("text") % f'*:"{attr}"*')
escaped_attr = json.dumps(attr, ensure_ascii=True)[1:-1]
if escaped_attr != attr:
attribute_clauses.append(Event.data.cast("text") % f'*:"{escaped_attr}"*')
if not attribute_clauses:
return None
return reduce(operator.or_, attribute_clauses)
@router.get(
"/events",
response_model=list[EventResponse],
@@ -212,9 +193,14 @@ def events(
if attributes != "all":
# Custom classification results are stored as data[model_name] = result_value
attribute_clause = _build_attribute_filter_clause(attributes)
if attribute_clause is not None:
clauses.append(attribute_clause)
filtered_attributes = attributes.split(",")
attribute_clauses = []
for attr in filtered_attributes:
attribute_clauses.append(Event.data.cast("text") % f'*:"{attr}"*')
attribute_clause = reduce(operator.or_, attribute_clauses)
clauses.append(attribute_clause)
if recognized_license_plate != "all":
filtered_recognized_license_plates = recognized_license_plate.split(",")
@@ -522,7 +508,7 @@ def events_search(
cameras = params.cameras
labels = params.labels
sub_labels = params.sub_labels
attributes = unquote(params.attributes)
attributes = params.attributes
zones = params.zones
after = params.after
before = params.before
@@ -621,9 +607,13 @@ def events_search(
if attributes != "all":
# Custom classification results are stored as data[model_name] = result_value
attribute_clause = _build_attribute_filter_clause(attributes)
if attribute_clause is not None:
event_filters.append(attribute_clause)
filtered_attributes = attributes.split(",")
attribute_clauses = []
for attr in filtered_attributes:
attribute_clauses.append(Event.data.cast("text") % f'*:"{attr}"*')
event_filters.append(reduce(operator.or_, attribute_clauses))
if zones != "all":
zone_clauses = []
@@ -658,7 +658,6 @@ class CustomObjectClassificationProcessor(RealTimeProcessorApi):
def handle_request(self, topic, request_data):
if topic == EmbeddingsRequestEnum.reload_classification_model.value:
if request_data.get("model_name") == self.model_config.name:
self.__build_detector()
logger.info(
f"Successfully loaded updated model for {self.model_config.name}"
)
-13
View File
@@ -6,7 +6,6 @@ from typing import Dict
from frigate.comms.events_updater import EventEndPublisher, EventUpdateSubscriber
from frigate.config import FrigateConfig
from frigate.config.classification import ObjectClassificationType
from frigate.events.types import EventStateEnum, EventTypeEnum
from frigate.models import Event
from frigate.util.builtin import to_relative_box
@@ -248,18 +247,6 @@ class EventProcessor(threading.Thread):
"recognized_license_plate"
][1]
# only overwrite attribute-type custom model fields in the database if they're set
for name, model_config in self.config.classification.custom.items():
if (
model_config.object_config
and model_config.object_config.classification_type
== ObjectClassificationType.attribute
):
value = event_data.get(name)
if value is not None:
event[Event.data][name] = value[0]
event[Event.data][f"{name}_score"] = value[1]
(
Event.insert(event)
.on_conflict(
-51
View File
@@ -168,57 +168,6 @@ class TestHttpApp(BaseTestHttp):
assert events[0]["id"] == id
assert events[1]["id"] == id2
def test_get_event_list_match_multilingual_attribute(self):
event_id = "123456.zh"
attribute = "中文标签"
with AuthTestClient(self.app) as client:
super().insert_mock_event(event_id, data={"custom_attr": attribute})
events = client.get("/events", params={"attributes": attribute}).json()
assert len(events) == 1
assert events[0]["id"] == event_id
events = client.get(
"/events", params={"attributes": "%E4%B8%AD%E6%96%87%E6%A0%87%E7%AD%BE"}
).json()
assert len(events) == 1
assert events[0]["id"] == event_id
def test_events_search_match_multilingual_attribute(self):
event_id = "123456.zh.search"
attribute = "中文标签"
mock_embeddings = Mock()
mock_embeddings.search_thumbnail.return_value = [(event_id, 0.05)]
self.app.frigate_config.semantic_search.enabled = True
self.app.embeddings = mock_embeddings
with AuthTestClient(self.app) as client:
super().insert_mock_event(event_id, data={"custom_attr": attribute})
events = client.get(
"/events/search",
params={
"search_type": "similarity",
"event_id": event_id,
"attributes": attribute,
},
).json()
assert len(events) == 1
assert events[0]["id"] == event_id
events = client.get(
"/events/search",
params={
"search_type": "similarity",
"event_id": event_id,
"attributes": "%E4%B8%AD%E6%96%87%E6%A0%87%E7%AD%BE",
},
).json()
assert len(events) == 1
assert events[0]["id"] == event_id
def test_get_good_event(self):
id = "123456.random"
+1 -10
View File
@@ -33,7 +33,6 @@ from frigate.config.camera.updater import (
CameraConfigUpdateEnum,
CameraConfigUpdateSubscriber,
)
from frigate.config.classification import ObjectClassificationType
from frigate.const import (
FAST_QUEUE_TIMEOUT,
UPDATE_CAMERA_ACTIVITY,
@@ -760,16 +759,8 @@ class TrackedObjectProcessor(threading.Thread):
self.update_mqtt_motion(camera, frame_time, motion_boxes)
attribute_model_names = [
name
for name, model_config in self.config.classification.custom.items()
if model_config.object_config
and model_config.object_config.classification_type
== ObjectClassificationType.attribute
]
tracked_objects = [
o.to_dict(attribute_model_names=attribute_model_names)
for o in camera_state.tracked_objects.values()
o.to_dict() for o in camera_state.tracked_objects.values()
]
# publish info on this frame
+1 -9
View File
@@ -376,10 +376,7 @@ class TrackedObject:
)
return (thumb_update, significant_change, path_update, autotracker_update)
def to_dict(
self,
attribute_model_names: list[str] | None = None,
) -> dict[str, Any]:
def to_dict(self) -> dict[str, Any]:
event = {
"id": self.obj_data["id"],
"camera": self.camera_config.name,
@@ -414,11 +411,6 @@ class TrackedObject:
"path_data": self.path_data.copy(),
"recognized_license_plate": self.obj_data.get("recognized_license_plate"),
}
if attribute_model_names is not None:
for name in attribute_model_names:
value = self.obj_data.get(name)
if value is not None:
event[name] = value
return event
@@ -70,12 +70,14 @@ export function AnimatedEventCard({
`${formatList(
[
...new Set([
...(event.data.objects || []),
...(event.data.objects || []).map((text) =>
text.replace("-verified", ""),
),
...(event.data.sub_labels || []),
...(event.data.audio || []),
]),
]
.filter((item) => item !== undefined && !item.includes("-verified"))
.filter((item) => item !== undefined)
.map((text) => getTranslatedLabel(text, getEventType(text)))
.sort(),
)} ` + t("detected")
+4 -4
View File
@@ -207,14 +207,14 @@ export default function ReviewCard({
{formatList(
[
...new Set([
...(event.data.objects || []),
...(event.data.objects || []).map((text) =>
text.replace("-verified", ""),
),
...(event.data.sub_labels || []),
...(event.data.audio || []),
]),
]
.filter(
(item) => item !== undefined && !item.includes("-verified"),
)
.filter((item) => item !== undefined)
.map((text) => getTranslatedLabel(text, getEventType(text)))
.sort(),
)}
@@ -297,15 +297,14 @@ export default function PreviewThumbnailPlayer({
: formatList(
[
...new Set([
...(review.data.objects || []),
...(review.data.objects || []).map((text) =>
text.replace("-verified", ""),
),
...(review.data.sub_labels || []),
...(review.data.audio || []),
]),
]
.filter(
(item) =>
item !== undefined && !item.includes("-verified"),
)
.filter((item) => item !== undefined)
.map((text) =>
getTranslatedLabel(text, getEventType(text)),
)
+3 -8
View File
@@ -33,12 +33,7 @@ import {
useRef,
useState,
} from "react";
import {
isDesktop,
isMobile,
isMobileOnly,
isTablet,
} from "react-device-detect";
import { isDesktop, isMobile } from "react-device-detect";
import { IoMdArrowRoundBack } from "react-icons/io";
import { useNavigate } from "react-router-dom";
import { Toaster } from "@/components/ui/sonner";
@@ -743,7 +738,7 @@ export function RecordingView({
aspectRatio: getCameraAspect(mainCamera),
}}
>
{(isDesktop || isTablet) && (
{isDesktop && (
<GenAISummaryDialog
review={activeReviewItem}
onOpen={onAnalysisOpen}
@@ -1006,7 +1001,7 @@ function Timeline({
),
)}
>
{isMobileOnly && timelineType == "timeline" && (
{isMobile && timelineType == "timeline" && (
<GenAISummaryDialog review={activeReviewItem} onOpen={onAnalysisOpen}>
<GenAISummaryChip review={activeReviewItem} />
</GenAISummaryDialog>