diff --git a/invokeai/app/api/routers/models.py b/invokeai/app/api/routers/models.py index 923a3767a3..683b12fbd5 100644 --- a/invokeai/app/api/routers/models.py +++ b/invokeai/app/api/routers/models.py @@ -63,20 +63,35 @@ async def update_model( ) -> UpdateModelResponse: """ Update model contents with a new config. If the model name or base fields are changed, then the model is renamed. """ logger = ApiDependencies.invoker.services.logger + + try: + previous_info = ApiDependencies.invoker.services.model_manager.list_model( + model_name=model_name, + base_model=base_model, + model_type=model_type, + ) + # rename operation requested if info.model_name != model_name or info.base_model != base_model: - result = ApiDependencies.invoker.services.model_manager.rename_model( + ApiDependencies.invoker.services.model_manager.rename_model( base_model = base_model, model_type = model_type, model_name = model_name, new_name = info.model_name, new_base = info.base_model, ) - logger.debug(f'renaming result = {result}') logger.info(f'Successfully renamed {base_model}/{model_name}=>{info.base_model}/{info.model_name}') + # update information to support an update of attributes model_name = info.model_name base_model = info.base_model + new_info = ApiDependencies.invoker.services.model_manager.list_model( + model_name=model_name, + base_model=base_model, + model_type=model_type, + ) + if new_info.get('path') != previous_info.get('path'): # model manager moved model path during rename - don't overwrite it + info.path = new_info.get('path') ApiDependencies.invoker.services.model_manager.update_model( model_name=model_name, diff --git a/invokeai/backend/model_management/model_manager.py b/invokeai/backend/model_management/model_manager.py index c62f42b88d..30eecbd26f 100644 --- a/invokeai/backend/model_management/model_manager.py +++ b/invokeai/backend/model_management/model_manager.py @@ -568,6 +568,9 @@ class ModelManager(object): model_type=cur_model_type, ) + # expose paths as absolute to help web UI + if path := model_dict.get('path'): + model_dict['path'] = str(self.app_config.root_path / path) models.append(model_dict) return models @@ -635,6 +638,10 @@ class ModelManager(object): The returned dict has the same format as the dict returned by model_info(). """ + # relativize paths as they go in - this makes it easier to move the root directory around + if path := model_attributes.get('path'): + if Path(path).is_relative_to(self.app_config.root_path): + model_attributes['path'] = str(Path(path).relative_to(self.app_config.root_path)) model_class = MODEL_CLASSES[base_model][model_type] model_config = model_class.create_config(**model_attributes) @@ -700,7 +707,7 @@ class ModelManager(object): # if this is a model file/directory that we manage ourselves, we need to move it if old_path.is_relative_to(self.app_config.models_path): - new_path = self.app_config.root_path / 'models' / new_base.value / model_type.value / new_name + new_path = self.app_config.root_path / 'models' / BaseModelType(new_base).value / ModelType(model_type).value / new_name move(old_path, new_path) model_cfg.path = str(new_path.relative_to(self.app_config.root_path)) diff --git a/invokeai/backend/model_management/models/vae.py b/invokeai/backend/model_management/models/vae.py index 2a5b7cff24..f740615509 100644 --- a/invokeai/backend/model_management/models/vae.py +++ b/invokeai/backend/model_management/models/vae.py @@ -16,6 +16,7 @@ from .base import ( calc_model_size_by_data, classproperty, InvalidModelException, + ModelNotFoundException, ) from invokeai.app.services.config import InvokeAIAppConfig from diffusers.utils import is_safetensors_available diff --git a/invokeai/frontend/web/public/locales/en.json b/invokeai/frontend/web/public/locales/en.json index d573175fe8..37a6f0ed49 100644 --- a/invokeai/frontend/web/public/locales/en.json +++ b/invokeai/frontend/web/public/locales/en.json @@ -399,6 +399,8 @@ "deleteModel": "Delete Model", "deleteConfig": "Delete Config", "deleteMsg1": "Are you sure you want to delete this model from InvokeAI?", + "modelDeleted": "Model Deleted", + "modelDeleteFailed": "Failed to delete model", "deleteMsg2": "This WILL delete the model from disk if it is in the InvokeAI root folder. If you are using a custom location, then the model WILL NOT be deleted from disk.", "formMessageDiffusersModelLocation": "Diffusers Model Location", "formMessageDiffusersModelLocationDesc": "Please enter at least one.", @@ -408,11 +410,13 @@ "convertToDiffusers": "Convert To Diffusers", "convertToDiffusersHelpText1": "This model will be converted to the 🧨 Diffusers format.", "convertToDiffusersHelpText2": "This process will replace your Model Manager entry with the Diffusers version of the same model.", - "convertToDiffusersHelpText3": "Your checkpoint file on the disk will NOT be deleted or modified in anyway. You can add your checkpoint to the Model Manager again if you want to.", + "convertToDiffusersHelpText3": "Your checkpoint file on disk WILL be deleted if it is in InvokeAI root folder. If it is in a custom location, then it WILL NOT be deleted.", "convertToDiffusersHelpText4": "This is a one time process only. It might take around 30s-60s depending on the specifications of your computer.", "convertToDiffusersHelpText5": "Please make sure you have enough disk space. Models generally vary between 2GB-7GB in size.", "convertToDiffusersHelpText6": "Do you wish to convert this model?", "convertToDiffusersSaveLocation": "Save Location", + "noCustomLocationProvided": "No Custom Location Provided", + "convertingModelBegin": "Converting Model. Please wait.", "v1": "v1", "v2_base": "v2 (512px)", "v2_768": "v2 (768px)", @@ -450,7 +454,8 @@ "none": "none", "addDifference": "Add Difference", "pickModelType": "Pick Model Type", - "selectModel": "Select Model" + "selectModel": "Select Model", + "importModels": "Import Models" }, "parameters": { "general": "General", diff --git a/invokeai/frontend/web/src/app/store/store.ts b/invokeai/frontend/web/src/app/store/store.ts index 2bafd21a74..4725a2f921 100644 --- a/invokeai/frontend/web/src/app/store/store.ts +++ b/invokeai/frontend/web/src/app/store/store.ts @@ -21,6 +21,7 @@ import generationReducer from 'features/parameters/store/generationSlice'; import postprocessingReducer from 'features/parameters/store/postprocessingSlice'; import configReducer from 'features/system/store/configSlice'; import systemReducer from 'features/system/store/systemSlice'; +import modelmanagerReducer from 'features/ui/components/tabs/ModelManager/store/modelManagerSlice'; import hotkeysReducer from 'features/ui/store/hotkeysSlice'; import uiReducer from 'features/ui/store/uiSlice'; @@ -49,6 +50,7 @@ const allReducers = { dynamicPrompts: dynamicPromptsReducer, imageDeletion: imageDeletionReducer, lora: loraReducer, + modelmanager: modelmanagerReducer, [api.reducerPath]: api.reducer, }; @@ -67,6 +69,7 @@ const rememberedKeys: (keyof typeof allReducers)[] = [ 'controlNet', 'dynamicPrompts', 'lora', + 'modelmanager', ]; export const store = configureStore({ diff --git a/invokeai/frontend/web/src/common/components/IAIInput.tsx b/invokeai/frontend/web/src/common/components/IAIInput.tsx index d114fc5968..31dac20998 100644 --- a/invokeai/frontend/web/src/common/components/IAIInput.tsx +++ b/invokeai/frontend/web/src/common/components/IAIInput.tsx @@ -8,19 +8,34 @@ import { import { useAppDispatch } from 'app/store/storeHooks'; import { stopPastePropagation } from 'common/util/stopPastePropagation'; import { shiftKeyPressed } from 'features/ui/store/hotkeysSlice'; -import { ChangeEvent, KeyboardEvent, memo, useCallback } from 'react'; +import { + CSSProperties, + ChangeEvent, + KeyboardEvent, + memo, + useCallback, +} from 'react'; interface IAIInputProps extends InputProps { label?: string; + labelPos?: 'top' | 'side'; value?: string; size?: string; onChange?: (e: ChangeEvent) => void; formControlProps?: Omit; } +const labelPosVerticalStyle: CSSProperties = { + display: 'flex', + flexDirection: 'row', + alignItems: 'center', + gap: 10, +}; + const IAIInput = (props: IAIInputProps) => { const { label = '', + labelPos = 'top', isDisabled = false, isInvalid, formControlProps, @@ -51,6 +66,7 @@ const IAIInput = (props: IAIInputProps) => { isInvalid={isInvalid} isDisabled={isDisabled} {...formControlProps} + style={labelPos === 'side' ? labelPosVerticalStyle : undefined} > {label !== '' && {label}} & { +export type IAISelectProps = Omit & { tooltip?: string; inputRef?: RefObject; label?: string; }; const IAIMantineSelect = (props: IAISelectProps) => { - const { tooltip, inputRef, label, disabled, ...rest } = props; + const { tooltip, inputRef, label, disabled, required, ...rest } = props; const styles = useMantineSelectStyles(); @@ -25,7 +25,7 @@ const IAIMantineSelect = (props: IAISelectProps) => {