From 05d39f79b025f2c2a4a76af171996b861f47d243 Mon Sep 17 00:00:00 2001 From: leccelecce <24962424+leccelecce@users.noreply.github.com> Date: Mon, 24 Mar 2025 15:19:58 +0000 Subject: [PATCH] Add ability to update Frigate+ model to latest from UI (#17324) * Add ability to update Frigate+ model to latest * UI tweaks * further UI tweaks * UI tweaks: add width and height, fix select * Add placeholder while API call in progress * Fix Frigate+ enabled check * Fix config change lost when reloading page * Add persistent message requiring restart * Drop down supported detectors and dimensions * Add width and height to display * Update FrigatePlusSettingsView.tsx * Temp fix for Codespaces not loading * Add i18n, format * remove unneeded brackets * missing colon * Revert "Temp fix for Codespaces not loading" This reverts commit 75b19674ce3c33e69308358c29e80bf2774f377d. --- frigate/api/app.py | 42 +++ frigate/plus.py | 8 + web/public/locales/en/views/settings.json | 11 +- web/src/pages/Settings.tsx | 4 +- web/src/types/frigateConfig.ts | 2 + .../settings/FrigatePlusSettingsView.tsx | 302 +++++++++++++++++- 6 files changed, 356 insertions(+), 13 deletions(-) diff --git a/frigate/api/app.py b/frigate/api/app.py index 0d391035e..8a1310b93 100644 --- a/frigate/api/app.py +++ b/frigate/api/app.py @@ -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: diff --git a/frigate/plus.py b/frigate/plus.py index 8ec578c64..197b6e48d 100644 --- a/frigate/plus.py +++ b/frigate/plus.py @@ -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() diff --git a/web/public/locales/en/views/settings.json b/web/public/locales/en/views/settings.json index ed9d291a1..d642c12e6 100644 --- a/web/public/locales/en/views/settings.json +++ b/web/public/locales/en/views/settings.json @@ -540,14 +540,21 @@ }, "modelInfo": { "title": "Model Information", - "modelId": "Model ID", "modelType": "Model Type", "trainDate": "Train Date", "baseModel": "Base Model", "supportedDetectors": "Supported Detectors", + "dimensions": "Dimensions", "cameras": "Cameras", "loading": "Loading model information...", - "error": "Failed to load model information" + "error": "Failed to load model information", + "availableModels": "Available Models", + "loadingAvailableModels": "Loading available models...", + "modelSelect": "Your available models on Frigate+ can be selected here. Note that only models compatible with your current detector configuration can be selected." + }, + "toast": { + "success": "Frigate+ settings have been saved. Restart Frigate to apply changes.", + "error": "Failed to save config changes: {{errorMessage}}" } } } diff --git a/web/src/pages/Settings.tsx b/web/src/pages/Settings.tsx index b00d3255c..3588f6491 100644 --- a/web/src/pages/Settings.tsx +++ b/web/src/pages/Settings.tsx @@ -278,7 +278,9 @@ export default function Settings() { {page == "notifications" && ( )} - {page == "frigateplus" && } + {page == "frigateplus" && ( + + )} {confirmationDialogOpen && ( ("config"); +type FrigatePlusModel = { + id: string; + type: string; + supportedDetectors: string[]; + trainDate: string; + baseModel: string; + width: number; + height: number; +}; + +type FrigatePlusSettings = { + model: { + id?: string; + }; +}; + +type FrigateSettingsViewProps = { + setUnsavedChanges: React.Dispatch>; +}; + +export default function FrigatePlusSettingsView({ + setUnsavedChanges, +}: FrigateSettingsViewProps) { const { t } = useTranslation("views/settings"); + const { data: config, mutate: updateConfig } = + useSWR("config"); + const [changedValue, setChangedValue] = useState(false); + const [isLoading, setIsLoading] = useState(false); + + const { addMessage, removeMessage } = useContext(StatusBarMessagesContext)!; + + const [frigatePlusSettings, setFrigatePlusSettings] = + useState({ + model: { + id: undefined, + }, + }); + + const [origPlusSettings, setOrigPlusSettings] = useState( + { + model: { + id: undefined, + }, + }, + ); + + const { data: availableModels = {} } = useSWR< + Record + >("/plus/models", { + fallbackData: {}, + fetcher: async (url) => { + const res = await axios.get(url, { withCredentials: true }); + return res.data.reduce( + (obj: Record, model: FrigatePlusModel) => { + obj[model.id] = model; + return obj; + }, + {}, + ); + }, + }); + + 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, + ) => { + 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}`, { + requires_restart: 0, + }) + .then((res) => { + if (res.status === 200) { + toast.success(t("frigatePlus.toast.success"), { + position: "top-center", + }); + setChangedValue(false); + addMessage( + "plus_restart", + "Restart required (Frigate+ model changed)", + undefined, + "plus_restart", + ); + 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, addMessage, 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 +198,10 @@ export default function FrigatePlusSettingsView() { ); }; + if (!config) { + return ; + } + return ( <>
@@ -101,7 +275,13 @@ export default function FrigatePlusSettingsView() { -

{config.model.plus.name}

+

+ {config.model.plus.name} ( + {config.model.plus.width + + "x" + + config.model.plus.height} + ) +

-
- -

{config.model.plus.id}

-
+
+
+
+ {t("frigatePlus.modelInfo.availableModels")} +
+
+

+ + frigatePlus.modelInfo.modelSelect + +

+
+
+ +
)} @@ -227,6 +481,34 @@ export default function FrigatePlusSettingsView() { )} + + + +
+ + +