mirror of
https://github.com/blakeblackshear/frigate.git
synced 2026-08-02 00:52:17 +03:00
Compare commits
5
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e21565a209 | ||
|
|
e658a70e0f | ||
|
|
82cb69526b | ||
|
|
fe3677c7df | ||
|
|
1fec95f88e |
+16
-26
@@ -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}"
|
||||
)
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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"
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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(),
|
||||
)}
|
||||
|
||||
@@ -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)),
|
||||
)
|
||||
|
||||
@@ -26,5 +26,6 @@ export const supportedLanguageKeys = [
|
||||
"lt",
|
||||
"uk",
|
||||
"cs",
|
||||
"sk",
|
||||
"hu",
|
||||
];
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user