mirror of
https://github.com/blakeblackshear/frigate.git
synced 2025-05-30 01:16:42 +02:00
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
This commit is contained in:
parent
61aef0bff0
commit
fad62b996a
@ -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
|
||||
```
|
||||
|
@ -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"] = (
|
||||
|
@ -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 <code>clean_copy</code> 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 <code>clean_copy</code> in your snapshot config to be able to submit images from these cameras to Frigate+.",
|
||||
"table": {
|
||||
"camera": "Camera",
|
||||
"snapshots": "Snapshots",
|
||||
"cleanCopySnapshots": "<code>clean_copy</code> 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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -563,7 +563,7 @@ function ObjectDetailsTab({
|
||||
<div className="text-sm text-primary/40">{t("details.label")}</div>
|
||||
<div className="flex flex-row items-center gap-2 text-sm capitalize">
|
||||
{getIconForLabel(search.label, "size-4 text-primary")}
|
||||
{t("{search.label}", { ns: "objects" })}
|
||||
{t(search.label, { ns: "objects" })}
|
||||
{search.sub_label && ` (${search.sub_label})`}
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
|
@ -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" && (
|
||||
<NotificationView setUnsavedChanges={setUnsavedChanges} />
|
||||
)}
|
||||
{page == "frigateplus" && <FrigatePlusSettingsView />}
|
||||
</div>
|
||||
{confirmationDialogOpen && (
|
||||
<AlertDialog
|
||||
|
@ -391,6 +391,12 @@ export interface FrigateConfig {
|
||||
colormap: { [key: string]: [number, number, number] };
|
||||
attributes_map: { [key: string]: [string] };
|
||||
all_attributes: [string];
|
||||
plus?: {
|
||||
name: string;
|
||||
trainDate: string;
|
||||
baseModel: string;
|
||||
supportedDetectors: string[];
|
||||
};
|
||||
};
|
||||
|
||||
motion: Record<string, unknown> | null;
|
||||
|
229
web/src/views/settings/FrigatePlusSettingsView.tsx
Normal file
229
web/src/views/settings/FrigatePlusSettingsView.tsx
Normal file
@ -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<FrigateConfig>("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 (
|
||||
<>
|
||||
<div className="flex size-full flex-col md:flex-row">
|
||||
<Toaster position="top-center" closeButton={true} />
|
||||
<div className="scrollbar-container order-last mb-10 mt-2 flex h-full w-full flex-col overflow-y-auto rounded-lg border-[1px] border-secondary-foreground bg-background_alt p-2 md:order-none md:mb-0 md:mr-2 md:mt-0">
|
||||
<Heading as="h3" className="my-2">
|
||||
{t("frigatePlus.title")}
|
||||
</Heading>
|
||||
|
||||
<Separator className="my-2 flex bg-secondary" />
|
||||
|
||||
<Heading as="h4" className="my-2">
|
||||
{t("frigatePlus.apiKey.title")}
|
||||
</Heading>
|
||||
|
||||
<div className="mt-2 space-y-6">
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center gap-2">
|
||||
{config?.plus?.enabled ? (
|
||||
<CheckCircle2 className="h-5 w-5 text-green-500" />
|
||||
) : (
|
||||
<XCircle className="h-5 w-5 text-red-500" />
|
||||
)}
|
||||
<Label>
|
||||
{config?.plus?.enabled
|
||||
? t("frigatePlus.apiKey.validated")
|
||||
: t("frigatePlus.apiKey.notValidated")}
|
||||
</Label>
|
||||
</div>
|
||||
<div className="my-2 max-w-5xl text-sm text-muted-foreground">
|
||||
<p>{t("frigatePlus.apiKey.desc")}</p>
|
||||
{!config?.model.plus && (
|
||||
<>
|
||||
<div className="mt-2 flex items-center text-primary-variant">
|
||||
<Link
|
||||
to="https://frigate.video/plus"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="inline"
|
||||
>
|
||||
{t("frigatePlus.apiKey.plusLink")}
|
||||
<LuExternalLink className="ml-2 inline-flex size-3" />
|
||||
</Link>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{config?.model.plus && (
|
||||
<>
|
||||
<Separator className="my-2 flex bg-secondary" />
|
||||
<div className="mt-2 max-w-2xl">
|
||||
<Heading as="h4" className="my-2">
|
||||
{t("frigatePlus.modelInfo.title")}
|
||||
</Heading>
|
||||
<div className="mt-2 space-y-3">
|
||||
{!config?.model?.plus && (
|
||||
<p className="text-muted-foreground">
|
||||
{t("frigatePlus.modelInfo.loading")}
|
||||
</p>
|
||||
)}
|
||||
{config?.model?.plus === null && (
|
||||
<p className="text-danger">
|
||||
{t("frigatePlus.modelInfo.error")}
|
||||
</p>
|
||||
)}
|
||||
{config?.model?.plus && (
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<Label className="text-muted-foreground">
|
||||
{t("frigatePlus.modelInfo.modelType")}
|
||||
</Label>
|
||||
<p>{config.model.plus.name}</p>
|
||||
</div>
|
||||
<div>
|
||||
<Label className="text-muted-foreground">
|
||||
{t("frigatePlus.modelInfo.trainDate")}
|
||||
</Label>
|
||||
<p>
|
||||
{new Date(
|
||||
config.model.plus.trainDate,
|
||||
).toLocaleString()}
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<Label className="text-muted-foreground">
|
||||
{t("frigatePlus.modelInfo.baseModel")}
|
||||
</Label>
|
||||
<p>{config.model.plus.baseModel}</p>
|
||||
</div>
|
||||
<div>
|
||||
<Label className="text-muted-foreground">
|
||||
{t("frigatePlus.modelInfo.supportedDetectors")}
|
||||
</Label>
|
||||
<p>
|
||||
{config.model.plus.supportedDetectors.join(", ")}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
<Separator className="my-2 flex bg-secondary" />
|
||||
|
||||
<div className="mt-2 max-w-5xl">
|
||||
<Heading as="h4" className="my-2">
|
||||
{t("frigatePlus.snapshotConfig.title")}
|
||||
</Heading>
|
||||
<div className="mt-2 space-y-3">
|
||||
<div className="my-2 text-sm text-muted-foreground">
|
||||
<p>
|
||||
<Trans ns="views/settings">
|
||||
frigatePlus.snapshotConfig.desc
|
||||
</Trans>
|
||||
</p>
|
||||
<div className="mt-2 flex items-center text-primary-variant">
|
||||
<Link
|
||||
to="https://docs.frigate.video/configuration/plus/faq"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="inline"
|
||||
>
|
||||
{t("frigatePlus.snapshotConfig.documentation")}
|
||||
<LuExternalLink className="ml-2 inline-flex size-3" />
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
{config && (
|
||||
<div className="overflow-x-auto">
|
||||
<table className="max-w-2xl text-sm">
|
||||
<thead>
|
||||
<tr className="border-b border-secondary">
|
||||
<th className="px-4 py-2 text-left">
|
||||
{t("frigatePlus.snapshotConfig.table.camera")}
|
||||
</th>
|
||||
<th className="px-4 py-2 text-center">
|
||||
{t("frigatePlus.snapshotConfig.table.snapshots")}
|
||||
</th>
|
||||
<th className="px-4 py-2 text-center">
|
||||
<Trans ns="views/settings">
|
||||
frigatePlus.snapshotConfig.table.cleanCopySnapshots
|
||||
</Trans>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{Object.entries(config.cameras).map(
|
||||
([name, camera]) => (
|
||||
<tr
|
||||
key={name}
|
||||
className="border-b border-secondary"
|
||||
>
|
||||
<td className="px-4 py-2">{name}</td>
|
||||
<td className="px-4 py-2 text-center">
|
||||
{camera.snapshots.enabled ? (
|
||||
<CheckCircle2 className="mx-auto size-5 text-green-500" />
|
||||
) : (
|
||||
<XCircle className="mx-auto size-5 text-danger" />
|
||||
)}
|
||||
</td>
|
||||
<td className="px-4 py-2 text-center">
|
||||
{camera.snapshots?.enabled &&
|
||||
camera.snapshots?.clean_copy ? (
|
||||
<CheckCircle2 className="mx-auto size-5 text-green-500" />
|
||||
) : (
|
||||
<XCircle className="mx-auto size-5 text-danger" />
|
||||
)}
|
||||
</td>
|
||||
</tr>
|
||||
),
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
)}
|
||||
{needCleanSnapshots() && (
|
||||
<div className="mt-2 max-w-xl rounded-lg border border-secondary-foreground bg-secondary p-4 text-sm text-danger">
|
||||
<div className="flex items-center gap-2">
|
||||
<IoIosWarning className="mr-2 size-5 text-danger" />
|
||||
<div className="max-w-[85%] text-sm">
|
||||
<Trans ns="views/settings">
|
||||
frigatePlus.snapshotConfig.cleanCopyWarning
|
||||
</Trans>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
Loading…
Reference in New Issue
Block a user