Compare commits

..
5 Commits
Author SHA1 Message Date
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
14 changed files with 39 additions and 201 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
@@ -1,7 +1,6 @@
{
"restart": {
"title": "Are you sure you want to restart Frigate?",
"description": "This will briefly stop Frigate while it restarts.",
"button": "Restart",
"restarting": {
"title": "Frigate is Restarting",
@@ -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(),
)}
+1 -49
View File
@@ -42,20 +42,12 @@ import {
TooltipTrigger,
} from "@/components/ui/tooltip";
import { isDesktop, isMobile } from "react-device-detect";
import {
Drawer,
DrawerContent,
DrawerDescription,
DrawerTitle,
DrawerTrigger,
} from "../ui/drawer";
import { Drawer, DrawerContent, DrawerTrigger } from "../ui/drawer";
import {
Dialog,
DialogClose,
DialogContent,
DialogDescription,
DialogPortal,
DialogTitle,
DialogTrigger,
} from "../ui/dialog";
import { TooltipPortal } from "@radix-ui/react-tooltip";
@@ -202,16 +194,6 @@ export default function GeneralSettings({ className }: GeneralSettingsProps) {
: "max-h-[75dvh] overflow-hidden p-2"
}
>
{!isDesktop && (
<>
<DrawerTitle className="sr-only">
{t("menu.settings")}
</DrawerTitle>
<DrawerDescription className="sr-only">
{t("menu.settings")}
</DrawerDescription>
</>
)}
<div className="scrollbar-container w-full flex-col overflow-y-auto overflow-x-hidden">
{isMobile && (
<div className="mb-2">
@@ -373,16 +355,6 @@ export default function GeneralSettings({ className }: GeneralSettingsProps) {
: "scrollbar-container max-h-[75dvh] w-[92%] overflow-y-scroll rounded-lg md:rounded-2xl"
}
>
{!isDesktop && (
<>
<DialogTitle className="sr-only">
{t("menu.languages")}
</DialogTitle>
<DialogDescription className="sr-only">
{t("menu.languages")}
</DialogDescription>
</>
)}
<span tabIndex={0} className="sr-only" />
{languages.map(({ code, label }) => (
<MenuItem
@@ -423,16 +395,6 @@ export default function GeneralSettings({ className }: GeneralSettingsProps) {
isDesktop ? "" : "w-[92%] rounded-lg md:rounded-2xl"
}
>
{!isDesktop && (
<>
<DialogTitle className="sr-only">
{t("menu.darkMode.label")}
</DialogTitle>
<DialogDescription className="sr-only">
{t("menu.darkMode.label")}
</DialogDescription>
</>
)}
<span tabIndex={0} className="sr-only" />
<MenuItem
className={
@@ -510,16 +472,6 @@ export default function GeneralSettings({ className }: GeneralSettingsProps) {
isDesktop ? "" : "w-[92%] rounded-lg md:rounded-2xl"
}
>
{!isDesktop && (
<>
<DialogTitle className="sr-only">
{t("menu.theme.label")}
</DialogTitle>
<DialogDescription className="sr-only">
{t("menu.theme.label")}
</DialogDescription>
</>
)}
<span tabIndex={0} className="sr-only" />
{colorSchemes.map((scheme) => (
<MenuItem
@@ -4,7 +4,6 @@ import {
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
@@ -38,12 +37,6 @@ export default function RestartDialog({
const [restartingSheetOpen, setRestartingSheetOpen] = useState(false);
const [countdown, setCountdown] = useState(60);
const clearBodyPointerEvents = () => {
if (typeof document !== "undefined") {
document.body.style.pointerEvents = "";
}
};
useEffect(() => {
setRestartDialogOpen(isOpen);
}, [isOpen]);
@@ -81,25 +74,14 @@ export default function RestartDialog({
<>
<AlertDialog
open={restartDialogOpen}
onOpenChange={(open) => {
if (!open) {
setRestartDialogOpen(false);
onClose();
clearBodyPointerEvents();
}
onOpenChange={() => {
setRestartDialogOpen(false);
onClose();
}}
>
<AlertDialogContent
onCloseAutoFocus={(event) => {
event.preventDefault();
clearBodyPointerEvents();
}}
>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>{t("restart.title")}</AlertDialogTitle>
<AlertDialogDescription className="sr-only">
{t("restart.description")}
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>
@@ -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)),
)
+1
View File
@@ -26,5 +26,6 @@ export const supportedLanguageKeys = [
"lt",
"uk",
"cs",
"sk",
"hu",
];
+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>