diff --git a/frigate/api/app.py b/frigate/api/app.py index 0d391035e..9bbaab17b 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 758089b85..d542723ae 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..6e7db5074 100644 --- a/web/public/locales/en/views/settings.json +++ b/web/public/locales/en/views/settings.json @@ -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}}" } } } 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; + trainDate: string; +}; + +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("/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, + ) => { + 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 ; + } + return ( <> @@ -133,6 +289,47 @@ export default function FrigatePlusSettingsView() { {config.model.plus.supportedDetectors.join(", ")} + + + + {t("frigatePlus.modelInfo.modelId")} + + + + + frigatePlus.modelInfo.modelSelect + + + + + + handleFrigatePlusConfigChange({ + model: { id: value as string }, + }) + } + > + + {frigatePlusSettings.model.id} + + + + {availableModels?.map((model) => ( + + {model.id} ( + {new Date(model.trainDate).toLocaleString()} + ) + + ))} + + + + )} @@ -227,6 +424,34 @@ export default function FrigatePlusSettingsView() { )} + + + + + + {t("button.reset", { ns: "common" })} + + + {isLoading ? ( + + + {t("button.saving", { ns: "common" })} + + ) : ( + t("button.save", { ns: "common" }) + )} + +
+ + frigatePlus.modelInfo.modelSelect + +