Add ability to update Frigate+ model to latest

This commit is contained in:
leccelecce 2025-03-20 16:00:56 +00:00 committed by james
parent 1e45f63a7c
commit f6b722842a
5 changed files with 287 additions and 5 deletions

View File

@ -640,6 +640,48 @@ def get_sub_labels(split_joined: Optional[int] = None):
return JSONResponse(content=sub_labels)
@router.get("/plus/models")
def plusModels(request: Request, filterByCurrentModelDetector: bool = False):
if not request.app.frigate_config.plus_api.is_active:
return JSONResponse(
content=({"success": False, "message": "Frigate+ is not enabled"}),
status_code=400,
)
models: dict[any, any] = request.app.frigate_config.plus_api.get_models()
if not models["list"]:
return JSONResponse(
content=({"success": False, "message": "No models found"}),
status_code=400,
)
modelList = models["list"]
# current model type
modelType = request.app.frigate_config.model.model_type
# current detectorType for comparing to supportedDetectors
detectorType = list(request.app.frigate_config.detectors.values())[0].type
validModels = []
for model in sorted(
filter(
lambda m: (
not filterByCurrentModelDetector
or (detectorType in m["supportedDetectors"] and modelType in m["type"])
),
modelList,
),
key=(lambda m: m["trainDate"]),
reverse=True,
):
validModels.append(model)
return JSONResponse(content=validModels)
@router.get("/recognized_license_plates")
def get_recognized_license_plates(split_joined: Optional[int] = None):
try:

View File

@ -234,3 +234,11 @@ class PlusApi:
raise Exception(r.text)
return r.json()
def get_models(self) -> Any:
r = self._get("model/list")
if not r.ok:
raise Exception(r.text)
return r.json()

View File

@ -547,7 +547,12 @@
"supportedDetectors": "Supported Detectors",
"cameras": "Cameras",
"loading": "Loading model information...",
"error": "Failed to load model information"
"error": "Failed to load model information",
"modelSelect": "Your available models on Frigate+ can be selected here. Note that only models compatible with your current detector configuration are shown."
},
"toast": {
"success": "Frigate+ settings have been saved. Restart Frigate to apply changes.",
"error": "Failed to save config changes: {{errorMessage}}"
}
}
}

View File

@ -278,7 +278,9 @@ export default function Settings() {
{page == "notifications" && (
<NotificationView setUnsavedChanges={setUnsavedChanges} />
)}
{page == "frigateplus" && <FrigatePlusSettingsView />}
{page == "frigateplus" && (
<FrigatePlusSettingsView setUnsavedChanges={setUnsavedChanges} />
)}
</div>
{confirmationDialogOpen && (
<AlertDialog

View File

@ -1,19 +1,171 @@
import Heading from "@/components/ui/heading";
import { Label } from "@/components/ui/label";
import { useEffect } from "react";
import { useCallback, useContext, useEffect, useState } from "react";
import { Toaster } from "sonner";
import { Separator } from "../../components/ui/separator";
import ActivityIndicator from "@/components/indicators/activity-indicator";
import { toast } from "sonner";
import useSWR from "swr";
import axios from "axios";
import { FrigateConfig } from "@/types/frigateConfig";
import { CheckCircle2, XCircle } from "lucide-react";
import { Trans, useTranslation } from "react-i18next";
import { IoIosWarning } from "react-icons/io";
import { Button } from "@/components/ui/button";
import { Link } from "react-router-dom";
import { LuExternalLink } from "react-icons/lu";
import { StatusBarMessagesContext } from "@/context/statusbar-provider";
import {
Select,
SelectContent,
SelectGroup,
SelectItem,
SelectTrigger,
} from "@/components/ui/select";
export default function FrigatePlusSettingsView() {
const { data: config } = useSWR<FrigateConfig>("config");
type FrigatePlusModel = {
id: string;
trainDate: string;
};
type FrigatePlusSettings = {
model: {
id?: string;
};
};
type FrigateSettingsViewProps = {
setUnsavedChanges: React.Dispatch<React.SetStateAction<boolean>>;
};
export default function FrigatePlusSettingsView({
setUnsavedChanges,
}: FrigateSettingsViewProps) {
const { t } = useTranslation("views/settings");
const { data: config, mutate: updateConfig } =
useSWR<FrigateConfig>("config");
const [changedValue, setChangedValue] = useState(false);
const [isLoading, setIsLoading] = useState(false);
const { addMessage, removeMessage } = useContext(StatusBarMessagesContext)!;
const [frigatePlusSettings, setFrigatePlusSettings] =
useState<FrigatePlusSettings>({
model: {
id: undefined,
},
});
const [origPlusSettings, setOrigPlusSettings] = useState<FrigatePlusSettings>(
{
model: {
id: undefined,
},
},
);
const { data: availableModels } = useSWR<FrigatePlusModel[]>("/plus/models", {
fallbackData: [],
fetcher: (url) =>
axios
.get(url, {
params: { filterByCurrentModelDetector: true },
withCredentials: true,
})
.then((res) => res.data),
});
useEffect(() => {
if (config) {
if (frigatePlusSettings?.model.id == undefined) {
setFrigatePlusSettings({
model: {
id: config.model.plus?.id,
},
});
}
setOrigPlusSettings({
model: {
id: config.model.plus?.id,
},
});
}
// we know that these deps are correct
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [config]);
const handleFrigatePlusConfigChange = (
newConfig: Partial<FrigatePlusSettings>,
) => {
setFrigatePlusSettings((prevConfig) => ({
model: {
...prevConfig.model,
...newConfig.model,
},
}));
setUnsavedChanges(true);
setChangedValue(true);
};
const saveToConfig = useCallback(async () => {
setIsLoading(true);
axios
.put(`config/set?model.path=plus://${frigatePlusSettings.model.id}`, {})
.then((res) => {
if (res.status === 200) {
toast.success(t("frigatePlus.toast.success"), {
position: "top-center",
});
setChangedValue(false);
updateConfig();
} else {
toast.error(
t("frigatePlus.toast.error", { errorMessage: res.statusText }),
{
position: "top-center",
},
);
}
})
.catch((error) => {
const errorMessage =
error.response?.data?.message ||
error.response?.data?.detail ||
"Unknown error";
toast.error(
t("toast.save.error.title", { errorMessage, ns: "common" }),
{
position: "top-center",
},
);
})
.finally(() => {
setIsLoading(false);
});
}, [updateConfig, frigatePlusSettings, t]);
const onCancel = useCallback(() => {
setFrigatePlusSettings(origPlusSettings);
setChangedValue(false);
removeMessage("plus_settings", "plus_settings");
}, [origPlusSettings, removeMessage]);
useEffect(() => {
if (changedValue) {
addMessage(
"plus_settings",
`Unsaved Frigate+ settings changes`,
undefined,
"plus_settings",
);
} else {
removeMessage("plus_settings", "plus_settings");
}
// we know that these deps are correct
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [changedValue]);
useEffect(() => {
document.title = t("documentTitle.frigatePlus");
@ -28,6 +180,10 @@ export default function FrigatePlusSettingsView() {
);
};
if (!config) {
return <ActivityIndicator />;
}
return (
<>
<div className="flex size-full flex-col md:flex-row">
@ -133,6 +289,47 @@ export default function FrigatePlusSettingsView() {
{config.model.plus.supportedDetectors.join(", ")}
</p>
</div>
<div className="col-span-2">
<div className="space-y-2">
<div className="text-md">
{t("frigatePlus.modelInfo.modelId")}
</div>
<div className="space-y-3 text-sm text-muted-foreground">
<p>
<Trans ns="views/settings">
frigatePlus.modelInfo.modelSelect
</Trans>
</p>
</div>
</div>
<Select
value={frigatePlusSettings.model.id}
onValueChange={(value) =>
handleFrigatePlusConfigChange({
model: { id: value as string },
})
}
>
<SelectTrigger>
{frigatePlusSettings.model.id}
</SelectTrigger>
<SelectContent>
<SelectGroup>
{availableModels?.map((model) => (
<SelectItem
key={model.id}
className="cursor-pointer"
value={model.id}
>
{model.id} (
{new Date(model.trainDate).toLocaleString()}
)
</SelectItem>
))}
</SelectGroup>
</SelectContent>
</Select>
</div>
</div>
)}
</div>
@ -227,6 +424,34 @@ export default function FrigatePlusSettingsView() {
)}
</div>
</div>
<Separator className="my-2 flex bg-secondary" />
<div className="flex w-full flex-row items-center gap-2 pt-2 md:w-[25%]">
<Button
className="flex flex-1"
aria-label={t("button.reset", { ns: "common" })}
onClick={onCancel}
>
{t("button.reset", { ns: "common" })}
</Button>
<Button
variant="select"
disabled={!changedValue || isLoading}
className="flex flex-1"
aria-label="Save"
onClick={saveToConfig}
>
{isLoading ? (
<div className="flex flex-row items-center gap-2">
<ActivityIndicator />
<span>{t("button.saving", { ns: "common" })}</span>
</div>
) : (
t("button.save", { ns: "common" })
)}
</Button>
</div>
</div>
</div>
</div>