From fad62b996ad9b66420128265f65ce1d035c25db0 Mon Sep 17 00:00:00 2001 From: Josh Hawkins <32435876+hawkeye217@users.noreply.github.com> Date: Mon, 17 Mar 2025 13:44:57 -0500 Subject: [PATCH] Add Frigate+ pane to Settings UI (#17208) * add plus data to config api response * add fields to frontend type * add frigate+ page in settings * add docs * fix label in explore detail dialog --- docs/docs/plus/faq.md | 10 + frigate/api/app.py | 13 + web/public/locales/en/views/settings.json | 37 ++- .../overlay/detail/SearchDetailDialog.tsx | 2 +- web/src/pages/Settings.tsx | 3 + web/src/types/frigateConfig.ts | 6 + .../settings/FrigatePlusSettingsView.tsx | 229 ++++++++++++++++++ 7 files changed, 297 insertions(+), 3 deletions(-) create mode 100644 web/src/views/settings/FrigatePlusSettingsView.tsx diff --git a/docs/docs/plus/faq.md b/docs/docs/plus/faq.md index fb0cd2512..151eb3f60 100644 --- a/docs/docs/plus/faq.md +++ b/docs/docs/plus/faq.md @@ -22,3 +22,13 @@ Yes. Models and metadata are stored in the `model_cache` directory within the co ### Can I keep using my Frigate+ models even if I do not renew my subscription? Yes. Subscriptions to Frigate+ provide access to the infrastructure used to train the models. Models trained with your subscription are yours to keep and use forever. However, do note that the terms and conditions prohibit you from sharing, reselling, or creating derivative products from the models. + +### Why can't I submit images to Frigate+? + +If you've configured your API key and the Frigate+ Settings page in the UI shows that the key is active, you need to ensure that you've enabled both snapshots and `clean_copy` snapshots for the cameras you'd like to submit images for. Note that `clean_copy` is enabled by default when snapshots are enabled. + +```yaml +snapshots: + enabled: true + clean_copy: true +``` diff --git a/frigate/api/app.py b/frigate/api/app.py index 05013ed12..9d7b3768f 100644 --- a/frigate/api/app.py +++ b/frigate/api/app.py @@ -9,6 +9,7 @@ import traceback from datetime import datetime, timedelta from functools import reduce from io import StringIO +from pathlib import Path as FilePath from typing import Any, Optional import aiofiles @@ -174,6 +175,18 @@ def config(request: Request): config["model"]["all_attributes"] = config_obj.model.all_attributes config["model"]["non_logo_attributes"] = config_obj.model.non_logo_attributes + # Add model plus data if plus is enabled + if config["plus"]["enabled"]: + model_json_path = FilePath(config["model"]["path"]).with_suffix(".json") + try: + with open(model_json_path, "r") as f: + model_plus_data = json.load(f) + config["model"]["plus"] = model_plus_data + except FileNotFoundError: + config["model"]["plus"] = None + except json.JSONDecodeError: + config["model"]["plus"] = None + # use merged labelamp for detector_config in config["detectors"].values(): detector_config["model"]["labelmap"] = ( diff --git a/web/public/locales/en/views/settings.json b/web/public/locales/en/views/settings.json index f19ac5ee6..3d25b92c1 100644 --- a/web/public/locales/en/views/settings.json +++ b/web/public/locales/en/views/settings.json @@ -7,7 +7,8 @@ "masksAndZones": "Mask and Zone Editor - Frigate", "motionTuner": "Motion Tuner - Frigate", "object": "Object Settings - Frigate", - "general": "General Settings - Frigate" + "general": "General Settings - Frigate", + "frigatePlus": "Frigate+ Settings - Frigate" }, "menu": { "uiSettings": "UI Settings", @@ -17,7 +18,8 @@ "motionTuner": "Motion Tuner", "debug": "Debug", "users": "Users", - "notifications": "Notifications" + "notifications": "Notifications", + "frigateplus": "Frigate+" }, "dialog": { "unsavedChanges": { @@ -515,5 +517,36 @@ "registerFailed": "Failed to save notification registration." } } + }, + "frigatePlus": { + "title": "Frigate+ Settings", + "apiKey": { + "title": "Frigate+ API Key", + "validated": "Frigate+ API key is detected and validated", + "notValidated": "Frigate+ API key is not detected or not validated", + "desc": "The Frigate+ API key enables integration with the Frigate+ service.", + "plusLink": "Read more about Frigate+" + }, + "snapshotConfig": { + "title": "Snapshot Configuration", + "desc": "Submitting to Frigate+ requires both snapshots and clean_copy snapshots to be enabled in your config.", + "documentation": "Read the documentation", + "cleanCopyWarning": "Some cameras have snapshots enabled but have the clean copy disabled. You need to enable clean_copy in your snapshot config to be able to submit images from these cameras to Frigate+.", + "table": { + "camera": "Camera", + "snapshots": "Snapshots", + "cleanCopySnapshots": "clean_copy Snapshots" + } + }, + "modelInfo": { + "title": "Model Information", + "modelType": "Model Type", + "trainDate": "Train Date", + "baseModel": "Base Model", + "supportedDetectors": "Supported Detectors", + "cameras": "Cameras", + "loading": "Loading model information...", + "error": "Failed to load model information" + } } } diff --git a/web/src/components/overlay/detail/SearchDetailDialog.tsx b/web/src/components/overlay/detail/SearchDetailDialog.tsx index d74efdf6d..891ce88b1 100644 --- a/web/src/components/overlay/detail/SearchDetailDialog.tsx +++ b/web/src/components/overlay/detail/SearchDetailDialog.tsx @@ -563,7 +563,7 @@ function ObjectDetailsTab({
{t("details.label")}
{getIconForLabel(search.label, "size-4 text-primary")} - {t("{search.label}", { ns: "objects" })} + {t(search.label, { ns: "objects" })} {search.sub_label && ` (${search.sub_label})`} diff --git a/web/src/pages/Settings.tsx b/web/src/pages/Settings.tsx index 353d0dbf8..6ccda34f3 100644 --- a/web/src/pages/Settings.tsx +++ b/web/src/pages/Settings.tsx @@ -37,6 +37,7 @@ import AuthenticationView from "@/views/settings/AuthenticationView"; import NotificationView from "@/views/settings/NotificationsSettingsView"; import ClassificationSettingsView from "@/views/settings/ClassificationSettingsView"; import UiSettingsView from "@/views/settings/UiSettingsView"; +import FrigatePlusSettingsView from "@/views/settings/FrigatePlusSettingsView"; import { useSearchEffect } from "@/hooks/use-overlay-state"; import { useSearchParams } from "react-router-dom"; import { useInitialCameraState } from "@/api/ws"; @@ -54,6 +55,7 @@ const allSettingsViews = [ "debug", "users", "notifications", + "frigateplus", ] as const; type SettingsType = (typeof allSettingsViews)[number]; @@ -279,6 +281,7 @@ export default function Settings() { {page == "notifications" && ( )} + {page == "frigateplus" && }
{confirmationDialogOpen && ( | null; diff --git a/web/src/views/settings/FrigatePlusSettingsView.tsx b/web/src/views/settings/FrigatePlusSettingsView.tsx new file mode 100644 index 000000000..965843f09 --- /dev/null +++ b/web/src/views/settings/FrigatePlusSettingsView.tsx @@ -0,0 +1,229 @@ +import Heading from "@/components/ui/heading"; +import { Label } from "@/components/ui/label"; +import { useEffect } from "react"; +import { Toaster } from "sonner"; +import { Separator } from "../../components/ui/separator"; +import useSWR from "swr"; +import { FrigateConfig } from "@/types/frigateConfig"; +import { CheckCircle2, XCircle } from "lucide-react"; +import { Trans, useTranslation } from "react-i18next"; +import { IoIosWarning } from "react-icons/io"; +import { Link } from "react-router-dom"; +import { LuExternalLink } from "react-icons/lu"; + +export default function FrigatePlusSettingsView() { + const { data: config } = useSWR("config"); + const { t } = useTranslation("views/settings"); + + useEffect(() => { + document.title = t("documentTitle.frigatePlus"); + }, [t]); + + const needCleanSnapshots = () => { + if (!config) { + return false; + } + return Object.values(config.cameras).some( + (camera) => camera.snapshots.enabled && !camera.snapshots.clean_copy, + ); + }; + + return ( + <> +
+ +
+ + {t("frigatePlus.title")} + + + + + + {t("frigatePlus.apiKey.title")} + + +
+
+
+ {config?.plus?.enabled ? ( + + ) : ( + + )} + +
+
+

{t("frigatePlus.apiKey.desc")}

+ {!config?.model.plus && ( + <> +
+ + {t("frigatePlus.apiKey.plusLink")} + + +
+ + )} +
+
+ + {config?.model.plus && ( + <> + +
+ + {t("frigatePlus.modelInfo.title")} + +
+ {!config?.model?.plus && ( +

+ {t("frigatePlus.modelInfo.loading")} +

+ )} + {config?.model?.plus === null && ( +

+ {t("frigatePlus.modelInfo.error")} +

+ )} + {config?.model?.plus && ( +
+
+ +

{config.model.plus.name}

+
+
+ +

+ {new Date( + config.model.plus.trainDate, + ).toLocaleString()} +

+
+
+ +

{config.model.plus.baseModel}

+
+
+ +

+ {config.model.plus.supportedDetectors.join(", ")} +

+
+
+ )} +
+
+ + )} + + + +
+ + {t("frigatePlus.snapshotConfig.title")} + +
+
+

+ + frigatePlus.snapshotConfig.desc + +

+
+ + {t("frigatePlus.snapshotConfig.documentation")} + + +
+
+ {config && ( +
+ + + + + + + + + + {Object.entries(config.cameras).map( + ([name, camera]) => ( + + + + + + ), + )} + +
+ {t("frigatePlus.snapshotConfig.table.camera")} + + {t("frigatePlus.snapshotConfig.table.snapshots")} + + + frigatePlus.snapshotConfig.table.cleanCopySnapshots + +
{name} + {camera.snapshots.enabled ? ( + + ) : ( + + )} + + {camera.snapshots?.enabled && + camera.snapshots?.clean_copy ? ( + + ) : ( + + )} +
+
+ )} + {needCleanSnapshots() && ( +
+
+ +
+ + frigatePlus.snapshotConfig.cleanCopyWarning + +
+
+
+ )} +
+
+
+
+
+ + ); +}