Compare commits

...

3 Commits
ai-chat ... dev

Author SHA1 Message Date
Josh Hawkins
4dcd2968b3 consolidate attribute filtering to match non-english and url encoded values (#22002) 2026-02-14 08:33:17 -06:00
Nicolas Mowen
73c1e12faf Fix saving attributes for object to DB (#22000) 2026-02-14 07:40:08 -06:00
Josh Hawkins
5f93cee732 fix tooltips (#21989) 2026-02-13 07:22:28 -06:00
8 changed files with 120 additions and 30 deletions

View File

@@ -69,6 +69,25 @@ logger = logging.getLogger(__name__)
router = APIRouter(tags=[Tags.events]) 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( @router.get(
"/events", "/events",
response_model=list[EventResponse], response_model=list[EventResponse],
@@ -193,14 +212,9 @@ def events(
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
filtered_attributes = attributes.split(",") attribute_clause = _build_attribute_filter_clause(attributes)
attribute_clauses = [] if attribute_clause is not None:
clauses.append(attribute_clause)
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": if recognized_license_plate != "all":
filtered_recognized_license_plates = recognized_license_plate.split(",") filtered_recognized_license_plates = recognized_license_plate.split(",")
@@ -508,7 +522,7 @@ def events_search(
cameras = params.cameras cameras = params.cameras
labels = params.labels labels = params.labels
sub_labels = params.sub_labels sub_labels = params.sub_labels
attributes = params.attributes attributes = unquote(params.attributes)
zones = params.zones zones = params.zones
after = params.after after = params.after
before = params.before before = params.before
@@ -607,13 +621,9 @@ def events_search(
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
filtered_attributes = attributes.split(",") attribute_clause = _build_attribute_filter_clause(attributes)
attribute_clauses = [] if attribute_clause is not None:
event_filters.append(attribute_clause)
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": if zones != "all":
zone_clauses = [] zone_clauses = []

View File

@@ -6,6 +6,7 @@ from typing import Dict
from frigate.comms.events_updater import EventEndPublisher, EventUpdateSubscriber from frigate.comms.events_updater import EventEndPublisher, EventUpdateSubscriber
from frigate.config import FrigateConfig from frigate.config import FrigateConfig
from frigate.config.classification import ObjectClassificationType
from frigate.events.types import EventStateEnum, EventTypeEnum from frigate.events.types import EventStateEnum, EventTypeEnum
from frigate.models import Event from frigate.models import Event
from frigate.util.builtin import to_relative_box from frigate.util.builtin import to_relative_box
@@ -247,6 +248,18 @@ class EventProcessor(threading.Thread):
"recognized_license_plate" "recognized_license_plate"
][1] ][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) Event.insert(event)
.on_conflict( .on_conflict(

View File

@@ -168,6 +168,57 @@ class TestHttpApp(BaseTestHttp):
assert events[0]["id"] == id assert events[0]["id"] == id
assert events[1]["id"] == id2 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): def test_get_good_event(self):
id = "123456.random" id = "123456.random"

View File

@@ -33,6 +33,7 @@ from frigate.config.camera.updater import (
CameraConfigUpdateEnum, CameraConfigUpdateEnum,
CameraConfigUpdateSubscriber, CameraConfigUpdateSubscriber,
) )
from frigate.config.classification import ObjectClassificationType
from frigate.const import ( from frigate.const import (
FAST_QUEUE_TIMEOUT, FAST_QUEUE_TIMEOUT,
UPDATE_CAMERA_ACTIVITY, UPDATE_CAMERA_ACTIVITY,
@@ -759,8 +760,16 @@ class TrackedObjectProcessor(threading.Thread):
self.update_mqtt_motion(camera, frame_time, motion_boxes) 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 = [ tracked_objects = [
o.to_dict() for o in camera_state.tracked_objects.values() o.to_dict(attribute_model_names=attribute_model_names)
for o in camera_state.tracked_objects.values()
] ]
# publish info on this frame # publish info on this frame

View File

@@ -376,7 +376,10 @@ class TrackedObject:
) )
return (thumb_update, significant_change, path_update, autotracker_update) return (thumb_update, significant_change, path_update, autotracker_update)
def to_dict(self) -> dict[str, Any]: def to_dict(
self,
attribute_model_names: list[str] | None = None,
) -> dict[str, Any]:
event = { event = {
"id": self.obj_data["id"], "id": self.obj_data["id"],
"camera": self.camera_config.name, "camera": self.camera_config.name,
@@ -411,6 +414,11 @@ class TrackedObject:
"path_data": self.path_data.copy(), "path_data": self.path_data.copy(),
"recognized_license_plate": self.obj_data.get("recognized_license_plate"), "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 return event

View File

@@ -70,14 +70,12 @@ export function AnimatedEventCard({
`${formatList( `${formatList(
[ [
...new Set([ ...new Set([
...(event.data.objects || []).map((text) => ...(event.data.objects || []),
text.replace("-verified", ""),
),
...(event.data.sub_labels || []), ...(event.data.sub_labels || []),
...(event.data.audio || []), ...(event.data.audio || []),
]), ]),
] ]
.filter((item) => item !== undefined) .filter((item) => item !== undefined && !item.includes("-verified"))
.map((text) => getTranslatedLabel(text, getEventType(text))) .map((text) => getTranslatedLabel(text, getEventType(text)))
.sort(), .sort(),
)} ` + t("detected") )} ` + t("detected")

View File

@@ -207,14 +207,14 @@ export default function ReviewCard({
{formatList( {formatList(
[ [
...new Set([ ...new Set([
...(event.data.objects || []).map((text) => ...(event.data.objects || []),
text.replace("-verified", ""),
),
...(event.data.sub_labels || []), ...(event.data.sub_labels || []),
...(event.data.audio || []), ...(event.data.audio || []),
]), ]),
] ]
.filter((item) => item !== undefined) .filter(
(item) => item !== undefined && !item.includes("-verified"),
)
.map((text) => getTranslatedLabel(text, getEventType(text))) .map((text) => getTranslatedLabel(text, getEventType(text)))
.sort(), .sort(),
)} )}

View File

@@ -297,14 +297,15 @@ export default function PreviewThumbnailPlayer({
: formatList( : formatList(
[ [
...new Set([ ...new Set([
...(review.data.objects || []).map((text) => ...(review.data.objects || []),
text.replace("-verified", ""),
),
...(review.data.sub_labels || []), ...(review.data.sub_labels || []),
...(review.data.audio || []), ...(review.data.audio || []),
]), ]),
] ]
.filter((item) => item !== undefined) .filter(
(item) =>
item !== undefined && !item.includes("-verified"),
)
.map((text) => .map((text) =>
getTranslatedLabel(text, getEventType(text)), getTranslatedLabel(text, getEventType(text)),
) )