diff --git a/invokeai/app/api/routers/model_manager.py b/invokeai/app/api/routers/model_manager.py index 47e0fd314e..78ef965d5a 100644 --- a/invokeai/app/api/routers/model_manager.py +++ b/invokeai/app/api/routers/model_manager.py @@ -14,6 +14,7 @@ from starlette.exceptions import HTTPException from typing_extensions import Annotated from invokeai.app.services.model_install import ModelInstallJob +from invokeai.app.services.model_metadata.metadata_store_base import ModelMetadataChanges from invokeai.app.services.model_records import ( DuplicateModelException, InvalidModelException, @@ -32,6 +33,7 @@ from invokeai.backend.model_manager.config import ( ) from invokeai.backend.model_manager.merge import MergeInterpolationMethod, ModelMerger from invokeai.backend.model_manager.metadata import AnyModelRepoMetadata +from invokeai.backend.model_manager.metadata.metadata_base import BaseMetadata from invokeai.backend.model_manager.search import ModelSearch from ..dependencies import ApiDependencies @@ -243,6 +245,47 @@ async def get_model_metadata( return result +@model_manager_router.patch( + "/i/{key}/metadata", + operation_id="update_model_metadata", + responses={ + 201: { + "description": "The model metadata was updated successfully", + "content": {"application/json": {"example": example_model_metadata}}, + }, + 400: {"description": "Bad request"}, + }, +) +async def update_model_metadata( + key: str = Path(description="Key of the model repo metadata to fetch."), + changes: ModelMetadataChanges = Body(description="The changes"), +) -> Optional[AnyModelRepoMetadata]: + """Updates or creates a model metadata object.""" + record_store = ApiDependencies.invoker.services.model_manager.store + metadata_store = ApiDependencies.invoker.services.model_manager.store.metadata_store + + try: + original_metadata = record_store.get_metadata(key) + if original_metadata: + if changes.default_settings: + original_metadata.default_settings = changes.default_settings + + metadata_store.update_metadata(key, original_metadata) + else: + metadata_store.add_metadata( + key, BaseMetadata(name="", author="", default_settings=changes.default_settings) + ) + except Exception as e: + raise HTTPException( + status_code=500, + detail=f"An error occurred while updating the model metadata: {e}", + ) + + result: Optional[AnyModelRepoMetadata] = record_store.get_metadata(key) + + return result + + @model_manager_router.get( "/tags", operation_id="list_tags", diff --git a/invokeai/app/services/model_metadata/metadata_store_base.py b/invokeai/app/services/model_metadata/metadata_store_base.py index e0e4381b09..882575a4bf 100644 --- a/invokeai/app/services/model_metadata/metadata_store_base.py +++ b/invokeai/app/services/model_metadata/metadata_store_base.py @@ -4,9 +4,25 @@ Storage for Model Metadata """ from abc import ABC, abstractmethod -from typing import List, Set, Tuple +from typing import List, Optional, Set, Tuple +from pydantic import Field + +from invokeai.app.util.model_exclude_null import BaseModelExcludeNull from invokeai.backend.model_manager.metadata import AnyModelRepoMetadata +from invokeai.backend.model_manager.metadata.metadata_base import ModelDefaultSettings + + +class ModelMetadataChanges(BaseModelExcludeNull, extra="allow"): + """A set of changes to apply to model metadata. + Only limited changes are valid: + - `default_settings`: the user-configured default settings for this model + """ + + default_settings: Optional[ModelDefaultSettings] = Field( + default=None, description="The user-configured default settings for this model" + ) + """The user-configured default settings for this model""" class ModelMetadataStoreBase(ABC): diff --git a/invokeai/app/services/model_metadata/metadata_store_sql.py b/invokeai/app/services/model_metadata/metadata_store_sql.py index afe9d2c8c6..4f8170448f 100644 --- a/invokeai/app/services/model_metadata/metadata_store_sql.py +++ b/invokeai/app/services/model_metadata/metadata_store_sql.py @@ -179,44 +179,45 @@ class ModelMetadataStoreSQL(ModelMetadataStoreBase): ) return {x[0] for x in self._cursor.fetchall()} - def _update_tags(self, model_key: str, tags: Set[str]) -> None: + def _update_tags(self, model_key: str, tags: Optional[Set[str]]) -> None: """Update tags for the model referenced by model_key.""" - # remove previous tags from this model - self._cursor.execute( - """--sql - DELETE FROM model_tags - WHERE model_id=?; - """, - (model_key,), - ) + if tags: + # remove previous tags from this model + self._cursor.execute( + """--sql + DELETE FROM model_tags + WHERE model_id=?; + """, + (model_key,), + ) - for tag in tags: - self._cursor.execute( - """--sql - INSERT OR IGNORE INTO tags ( - tag_text - ) - VALUES (?); - """, - (tag,), - ) - self._cursor.execute( - """--sql - SELECT tag_id - FROM tags - WHERE tag_text = ? - LIMIT 1; - """, - (tag,), - ) - tag_id = self._cursor.fetchone()[0] - self._cursor.execute( - """--sql - INSERT OR IGNORE INTO model_tags ( - model_id, - tag_id - ) - VALUES (?,?); - """, - (model_key, tag_id), - ) + for tag in tags: + self._cursor.execute( + """--sql + INSERT OR IGNORE INTO tags ( + tag_text + ) + VALUES (?); + """, + (tag,), + ) + self._cursor.execute( + """--sql + SELECT tag_id + FROM tags + WHERE tag_text = ? + LIMIT 1; + """, + (tag,), + ) + tag_id = self._cursor.fetchone()[0] + self._cursor.execute( + """--sql + INSERT OR IGNORE INTO model_tags ( + model_id, + tag_id + ) + VALUES (?,?); + """, + (model_key, tag_id), + ) diff --git a/invokeai/backend/model_manager/metadata/metadata_base.py b/invokeai/backend/model_manager/metadata/metadata_base.py index 379369f9f5..5f062d0a04 100644 --- a/invokeai/backend/model_manager/metadata/metadata_base.py +++ b/invokeai/backend/model_manager/metadata/metadata_base.py @@ -25,6 +25,7 @@ from pydantic.networks import AnyHttpUrl from requests.sessions import Session from typing_extensions import Annotated +from invokeai.app.invocations.constants import SCHEDULER_NAME_VALUES from invokeai.backend.model_manager import ModelRepoVariant from ..util import select_hf_files @@ -68,12 +69,24 @@ class RemoteModelFile(BaseModel): sha256: Optional[str] = Field(description="SHA256 hash of this model (not always available)", default=None) +class ModelDefaultSettings(BaseModel): + vae: str | None + vae_precision: str | None + scheduler: SCHEDULER_NAME_VALUES | None + steps: int | None + cfg_scale: float | None + cfg_rescale_multiplier: float | None + + class ModelMetadataBase(BaseModel): """Base class for model metadata information.""" name: str = Field(description="model's name") author: str = Field(description="model's author") - tags: Set[str] = Field(description="tags provided by model source") + tags: Optional[Set[str]] = Field(description="tags provided by model source", default=None) + default_settings: Optional[ModelDefaultSettings] = Field( + description="default settings for this model", default=None + ) class BaseMetadata(ModelMetadataBase): diff --git a/invokeai/frontend/web/public/locales/en.json b/invokeai/frontend/web/public/locales/en.json index 4fe763922e..406a33d9e8 100644 --- a/invokeai/frontend/web/public/locales/en.json +++ b/invokeai/frontend/web/public/locales/en.json @@ -78,6 +78,7 @@ "aboutDesc": "Using Invoke for work? Check out:", "aboutHeading": "Own Your Creative Power", "accept": "Accept", + "add": "Add", "advanced": "Advanced", "advancedOptions": "Advanced Options", "ai": "ai", @@ -734,6 +735,8 @@ "customConfig": "Custom Config", "customConfigFileLocation": "Custom Config File Location", "customSaveLocation": "Custom Save Location", + "defaultSettings": "Default Settings", + "defaultSettingsSaved": "Default Settings Saved", "delete": "Delete", "deleteConfig": "Delete Config", "deleteModel": "Delete Model", @@ -768,6 +771,7 @@ "mergedModelName": "Merged Model Name", "mergedModelSaveLocation": "Save Location", "mergeModels": "Merge Models", + "metadata": "Metadata", "model": "Model", "modelAdded": "Model Added", "modelConversionFailed": "Model Conversion Failed", @@ -839,9 +843,12 @@ "statusConverting": "Converting", "syncModels": "Sync Models", "syncModelsDesc": "If your models are out of sync with the backend, you can refresh them up using this option. This is generally handy in cases where you add models to the InvokeAI root folder or autoimport directory after the application has booted.", + "triggerPhrases": "Trigger Phrases", + "typePhraseHere": "Type phrase here", "upcastAttention": "Upcast Attention", "updateModel": "Update Model", "useCustomConfig": "Use Custom Config", + "useDefaultSettings": "Use Default Settings", "v1": "v1", "v2_768": "v2 (768px)", "v2_base": "v2 (512px)", diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/index.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/index.ts index c5d86a127f..8e2715e3fa 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/index.ts +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/index.ts @@ -55,6 +55,8 @@ import { addUpscaleRequestedListener } from 'app/store/middleware/listenerMiddle import { addWorkflowLoadRequestedListener } from 'app/store/middleware/listenerMiddleware/listeners/workflowLoadRequested'; import type { AppDispatch, RootState } from 'app/store/store'; +import { addSetDefaultSettingsListener } from './listeners/setDefaultSettings'; + export const listenerMiddleware = createListenerMiddleware(); export type AppStartListening = TypedStartListening; @@ -153,3 +155,5 @@ addUpscaleRequestedListener(startAppListening); // Dynamic prompts addDynamicPromptsListener(startAppListening); + +addSetDefaultSettingsListener(startAppListening); diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/setDefaultSettings.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/setDefaultSettings.ts new file mode 100644 index 0000000000..cd4c574be4 --- /dev/null +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/setDefaultSettings.ts @@ -0,0 +1,96 @@ +import type { AppStartListening } from 'app/store/middleware/listenerMiddleware'; +import { setDefaultSettings } from 'features/parameters/store/actions'; +import { + setCfgRescaleMultiplier, + setCfgScale, + setScheduler, + setSteps, + vaePrecisionChanged, + vaeSelected, +} from 'features/parameters/store/generationSlice'; +import { + isParameterCFGRescaleMultiplier, + isParameterCFGScale, + isParameterPrecision, + isParameterScheduler, + isParameterSteps, + zParameterVAEModel, +} from 'features/parameters/types/parameterSchemas'; +import { addToast } from 'features/system/store/systemSlice'; +import { makeToast } from 'features/system/util/makeToast'; +import { t } from 'i18next'; +import { map } from 'lodash-es'; +import { modelsApi } from 'services/api/endpoints/models'; + +export const addSetDefaultSettingsListener = (startAppListening: AppStartListening) => { + startAppListening({ + actionCreator: setDefaultSettings, + effect: async (action, { dispatch, getState }) => { + const state = getState(); + + const currentModel = state.generation.model; + + if (!currentModel) { + return; + } + + const metadata = await dispatch(modelsApi.endpoints.getModelMetadata.initiate(currentModel.key)).unwrap(); + + if (!metadata || !metadata.default_settings) { + return; + } + + const { vae, vae_precision, cfg_scale, cfg_rescale_multiplier, steps, scheduler } = metadata.default_settings; + + if (vae) { + // we store this as "default" within default settings + // to distinguish it from no default set + if (vae === 'default') { + dispatch(vaeSelected(null)); + } else { + const { data } = modelsApi.endpoints.getVaeModels.select()(state); + const vaeArray = map(data?.entities); + const validVae = vaeArray.find((model) => model.key === vae); + + const result = zParameterVAEModel.safeParse(validVae); + if (!result.success) { + return; + } + dispatch(vaeSelected(result.data)); + } + } + + if (vae_precision) { + if (isParameterPrecision(vae_precision)) { + dispatch(vaePrecisionChanged(vae_precision)); + } + } + + if (cfg_scale) { + if (isParameterCFGScale(cfg_scale)) { + dispatch(setCfgScale(cfg_scale)); + } + } + + if (cfg_rescale_multiplier) { + if (isParameterCFGRescaleMultiplier(cfg_rescale_multiplier)) { + dispatch(setCfgRescaleMultiplier(cfg_rescale_multiplier)); + } + } + + if (steps) { + if (isParameterSteps(steps)) { + dispatch(setSteps(steps)); + } + } + + if (scheduler) { + if (isParameterScheduler(scheduler)) { + dispatch(setScheduler(scheduler)); + } + } + + dispatch(addToast(makeToast({ title: t('toast.parameterSet', { parameter: 'Default settings' }) }))); + }, + }); +}; diff --git a/invokeai/frontend/web/src/app/types/invokeai.ts b/invokeai/frontend/web/src/app/types/invokeai.ts index a2b17b483d..0092d0c99e 100644 --- a/invokeai/frontend/web/src/app/types/invokeai.ts +++ b/invokeai/frontend/web/src/app/types/invokeai.ts @@ -1,4 +1,5 @@ import type { CONTROLNET_PROCESSORS } from 'features/controlAdapters/store/constants'; +import type { ParameterPrecision, ParameterScheduler } from 'features/parameters/types/parameterSchemas'; import type { InvokeTabName } from 'features/ui/store/tabMap'; import type { O } from 'ts-toolbelt'; @@ -82,6 +83,8 @@ export type AppConfig = { guidance: NumericalParameterConfig; cfgRescaleMultiplier: NumericalParameterConfig; img2imgStrength: NumericalParameterConfig; + scheduler?: ParameterScheduler; + vaePrecision?: ParameterPrecision; // Canvas boundingBoxHeight: NumericalParameterConfig; // initial value comes from model boundingBoxWidth: NumericalParameterConfig; // initial value comes from model diff --git a/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelPane.tsx b/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelPane.tsx index 9cae8d2984..c19aceda11 100644 --- a/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelPane.tsx +++ b/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelPane.tsx @@ -8,7 +8,7 @@ export const ModelPane = () => { const selectedModelKey = useAppSelector((s) => s.modelmanagerV2.selectedModelKey); return ( - {selectedModelKey ? : } + {selectedModelKey ? : } ); }; diff --git a/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelPanel/DefaultSettings.tsx b/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelPanel/DefaultSettings.tsx new file mode 100644 index 0000000000..d45f33e390 --- /dev/null +++ b/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelPanel/DefaultSettings.tsx @@ -0,0 +1,66 @@ +import { skipToken } from '@reduxjs/toolkit/query'; +import { createMemoizedSelector } from 'app/store/createMemoizedSelector'; +import { useAppSelector } from 'app/store/storeHooks'; +import Loading from 'common/components/Loading/Loading'; +import { selectConfigSlice } from 'features/system/store/configSlice'; +import { isNil } from 'lodash-es'; +import { useMemo } from 'react'; +import { useGetModelMetadataQuery } from 'services/api/endpoints/models'; + +import { DefaultSettingsForm } from './DefaultSettings/DefaultSettingsForm'; + +const initialStatesSelector = createMemoizedSelector(selectConfigSlice, (config) => { + const { steps, guidance, scheduler, cfgRescaleMultiplier, vaePrecision } = config.sd; + + return { + initialSteps: steps.initial, + initialCfg: guidance.initial, + initialScheduler: scheduler, + initialCfgRescaleMultiplier: cfgRescaleMultiplier.initial, + initialVaePrecision: vaePrecision, + }; +}); + +export const DefaultSettings = () => { + const selectedModelKey = useAppSelector((s) => s.modelmanagerV2.selectedModelKey); + + const { data, isLoading } = useGetModelMetadataQuery(selectedModelKey ?? skipToken); + const { initialSteps, initialCfg, initialScheduler, initialCfgRescaleMultiplier, initialVaePrecision } = + useAppSelector(initialStatesSelector); + + const defaultSettingsDefaults = useMemo(() => { + return { + vae: { isEnabled: !isNil(data?.default_settings?.vae), value: data?.default_settings?.vae || 'default' }, + vaePrecision: { + isEnabled: !isNil(data?.default_settings?.vae_precision), + value: data?.default_settings?.vae_precision || initialVaePrecision || 'fp32', + }, + scheduler: { + isEnabled: !isNil(data?.default_settings?.scheduler), + value: data?.default_settings?.scheduler || initialScheduler || 'euler', + }, + steps: { isEnabled: !isNil(data?.default_settings?.steps), value: data?.default_settings?.steps || initialSteps }, + cfgScale: { + isEnabled: !isNil(data?.default_settings?.cfg_scale), + value: data?.default_settings?.cfg_scale || initialCfg, + }, + cfgRescaleMultiplier: { + isEnabled: !isNil(data?.default_settings?.cfg_rescale_multiplier), + value: data?.default_settings?.cfg_rescale_multiplier || initialCfgRescaleMultiplier, + }, + }; + }, [ + data?.default_settings, + initialSteps, + initialCfg, + initialScheduler, + initialCfgRescaleMultiplier, + initialVaePrecision, + ]); + + if (isLoading) { + return ; + } + + return ; +}; diff --git a/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelPanel/DefaultSettings/DefaultCfgRescaleMultiplier.tsx b/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelPanel/DefaultSettings/DefaultCfgRescaleMultiplier.tsx new file mode 100644 index 0000000000..fd88bab662 --- /dev/null +++ b/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelPanel/DefaultSettings/DefaultCfgRescaleMultiplier.tsx @@ -0,0 +1,72 @@ +import { CompositeNumberInput, CompositeSlider, Flex, FormControl, FormLabel } from '@invoke-ai/ui-library'; +import { useAppSelector } from 'app/store/storeHooks'; +import { InformationalPopover } from 'common/components/InformationalPopover/InformationalPopover'; +import { useCallback, useMemo } from 'react'; +import type { UseControllerProps } from 'react-hook-form'; +import { useController } from 'react-hook-form'; +import { useTranslation } from 'react-i18next'; + +import type { DefaultSettingsFormData } from './DefaultSettingsForm'; + +type DefaultCfgRescaleMultiplierType = DefaultSettingsFormData['cfgRescaleMultiplier']; + +export function DefaultCfgRescaleMultiplier(props: UseControllerProps) { + const { field } = useController(props); + + const sliderMin = useAppSelector((s) => s.config.sd.cfgRescaleMultiplier.sliderMin); + const sliderMax = useAppSelector((s) => s.config.sd.cfgRescaleMultiplier.sliderMax); + const numberInputMin = useAppSelector((s) => s.config.sd.cfgRescaleMultiplier.numberInputMin); + const numberInputMax = useAppSelector((s) => s.config.sd.cfgRescaleMultiplier.numberInputMax); + const coarseStep = useAppSelector((s) => s.config.sd.cfgRescaleMultiplier.coarseStep); + const fineStep = useAppSelector((s) => s.config.sd.cfgRescaleMultiplier.fineStep); + const { t } = useTranslation(); + const marks = useMemo(() => [sliderMin, Math.floor(sliderMax / 2), sliderMax], [sliderMax, sliderMin]); + + const onChange = useCallback( + (v: number) => { + const updatedValue = { + ...(field.value as DefaultCfgRescaleMultiplierType), + value: v, + }; + field.onChange(updatedValue); + }, + [field] + ); + + const value = useMemo(() => { + return (field.value as DefaultCfgRescaleMultiplierType).value; + }, [field.value]); + + const isDisabled = useMemo(() => { + return !(field.value as DefaultCfgRescaleMultiplierType).isEnabled; + }, [field.value]); + + return ( + + + {t('parameters.cfgRescaleMultiplier')} + + + + + + + ); +} diff --git a/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelPanel/DefaultSettings/DefaultCfgScale.tsx b/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelPanel/DefaultSettings/DefaultCfgScale.tsx new file mode 100644 index 0000000000..8e49517eb4 --- /dev/null +++ b/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelPanel/DefaultSettings/DefaultCfgScale.tsx @@ -0,0 +1,72 @@ +import { CompositeNumberInput, CompositeSlider, Flex, FormControl, FormLabel } from '@invoke-ai/ui-library'; +import { useAppSelector } from 'app/store/storeHooks'; +import { InformationalPopover } from 'common/components/InformationalPopover/InformationalPopover'; +import { useCallback, useMemo } from 'react'; +import type { UseControllerProps } from 'react-hook-form'; +import { useController } from 'react-hook-form'; +import { useTranslation } from 'react-i18next'; + +import type { DefaultSettingsFormData } from './DefaultSettingsForm'; + +type DefaultCfgType = DefaultSettingsFormData['cfgScale']; + +export function DefaultCfgScale(props: UseControllerProps) { + const { field } = useController(props); + + const sliderMin = useAppSelector((s) => s.config.sd.guidance.sliderMin); + const sliderMax = useAppSelector((s) => s.config.sd.guidance.sliderMax); + const numberInputMin = useAppSelector((s) => s.config.sd.guidance.numberInputMin); + const numberInputMax = useAppSelector((s) => s.config.sd.guidance.numberInputMax); + const coarseStep = useAppSelector((s) => s.config.sd.guidance.coarseStep); + const fineStep = useAppSelector((s) => s.config.sd.guidance.fineStep); + const { t } = useTranslation(); + const marks = useMemo(() => [sliderMin, Math.floor(sliderMax / 2), sliderMax], [sliderMax, sliderMin]); + + const onChange = useCallback( + (v: number) => { + const updatedValue = { + ...(field.value as DefaultCfgType), + value: v, + }; + field.onChange(updatedValue); + }, + [field] + ); + + const value = useMemo(() => { + return (field.value as DefaultCfgType).value; + }, [field.value]); + + const isDisabled = useMemo(() => { + return !(field.value as DefaultCfgType).isEnabled; + }, [field.value]); + + return ( + + + {t('parameters.cfgScale')} + + + + + + + ); +} diff --git a/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelPanel/DefaultSettings/DefaultScheduler.tsx b/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelPanel/DefaultSettings/DefaultScheduler.tsx new file mode 100644 index 0000000000..46b42fd873 --- /dev/null +++ b/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelPanel/DefaultSettings/DefaultScheduler.tsx @@ -0,0 +1,50 @@ +import type { ComboboxOnChange } from '@invoke-ai/ui-library'; +import { Combobox, FormControl, FormLabel } from '@invoke-ai/ui-library'; +import { InformationalPopover } from 'common/components/InformationalPopover/InformationalPopover'; +import { SCHEDULER_OPTIONS } from 'features/parameters/types/constants'; +import { isParameterScheduler } from 'features/parameters/types/parameterSchemas'; +import { useCallback, useMemo } from 'react'; +import type { UseControllerProps } from 'react-hook-form'; +import { useController } from 'react-hook-form'; +import { useTranslation } from 'react-i18next'; + +import type { DefaultSettingsFormData } from './DefaultSettingsForm'; + +type DefaultSchedulerType = DefaultSettingsFormData['scheduler']; + +export function DefaultScheduler(props: UseControllerProps) { + const { t } = useTranslation(); + const { field } = useController(props); + + const onChange = useCallback( + (v) => { + if (!isParameterScheduler(v?.value)) { + return; + } + const updatedValue = { + ...(field.value as DefaultSchedulerType), + value: v.value, + }; + field.onChange(updatedValue); + }, + [field] + ); + + const value = useMemo( + () => SCHEDULER_OPTIONS.find((o) => o.value === (field.value as DefaultSchedulerType).value), + [field] + ); + + const isDisabled = useMemo(() => { + return !(field.value as DefaultSchedulerType).isEnabled; + }, [field.value]); + + return ( + + + {t('parameters.scheduler')} + + + + ); +} diff --git a/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelPanel/DefaultSettings/DefaultSettingsForm.tsx b/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelPanel/DefaultSettings/DefaultSettingsForm.tsx new file mode 100644 index 0000000000..699e3e3445 --- /dev/null +++ b/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelPanel/DefaultSettings/DefaultSettingsForm.tsx @@ -0,0 +1,147 @@ +import { Button, Flex, Heading } from '@invoke-ai/ui-library'; +import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; +import type { ParameterScheduler } from 'features/parameters/types/parameterSchemas'; +import { addToast } from 'features/system/store/systemSlice'; +import { makeToast } from 'features/system/util/makeToast'; +import { useCallback } from 'react'; +import type { SubmitHandler } from 'react-hook-form'; +import { useForm } from 'react-hook-form'; +import { useTranslation } from 'react-i18next'; +import { IoPencil } from 'react-icons/io5'; +import { useUpdateModelMetadataMutation } from 'services/api/endpoints/models'; + +import { DefaultCfgRescaleMultiplier } from './DefaultCfgRescaleMultiplier'; +import { DefaultCfgScale } from './DefaultCfgScale'; +import { DefaultScheduler } from './DefaultScheduler'; +import { DefaultSteps } from './DefaultSteps'; +import { DefaultVae } from './DefaultVae'; +import { DefaultVaePrecision } from './DefaultVaePrecision'; +import { SettingToggle } from './SettingToggle'; + +export interface FormField { + value: T; + isEnabled: boolean; +} + +export type DefaultSettingsFormData = { + vae: FormField; + vaePrecision: FormField; + scheduler: FormField; + steps: FormField; + cfgScale: FormField; + cfgRescaleMultiplier: FormField; +}; + +export const DefaultSettingsForm = ({ + defaultSettingsDefaults, +}: { + defaultSettingsDefaults: DefaultSettingsFormData; +}) => { + const dispatch = useAppDispatch(); + const { t } = useTranslation(); + const selectedModelKey = useAppSelector((s) => s.modelmanagerV2.selectedModelKey); + + const [editModelMetadata, { isLoading }] = useUpdateModelMetadataMutation(); + + const { handleSubmit, control, formState } = useForm({ + defaultValues: defaultSettingsDefaults, + }); + + const onSubmit = useCallback>( + (data) => { + if (!selectedModelKey) { + return; + } + + const body = { + vae: data.vae.isEnabled ? data.vae.value : null, + vae_precision: data.vaePrecision.isEnabled ? data.vaePrecision.value : null, + cfg_scale: data.cfgScale.isEnabled ? data.cfgScale.value : null, + cfg_rescale_multiplier: data.cfgRescaleMultiplier.isEnabled ? data.cfgRescaleMultiplier.value : null, + steps: data.steps.isEnabled ? data.steps.value : null, + scheduler: data.scheduler.isEnabled ? data.scheduler.value : null, + }; + + editModelMetadata({ + key: selectedModelKey, + body: { default_settings: body }, + }) + .unwrap() + .then((_) => { + dispatch( + addToast( + makeToast({ + title: t('modelManager.defaultSettingsSaved'), + status: 'success', + }) + ) + ); + }) + .catch((error) => { + if (error) { + dispatch( + addToast( + makeToast({ + title: `${error.data.detail} `, + status: 'error', + }) + ) + ); + } + }); + }, + [selectedModelKey, dispatch, editModelMetadata, t] + ); + + return ( + <> + + {t('modelManager.defaultSettings')} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ); +}; diff --git a/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelPanel/DefaultSettings/DefaultSteps.tsx b/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelPanel/DefaultSettings/DefaultSteps.tsx new file mode 100644 index 0000000000..4ccef8fd73 --- /dev/null +++ b/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelPanel/DefaultSettings/DefaultSteps.tsx @@ -0,0 +1,72 @@ +import { CompositeNumberInput, CompositeSlider, Flex, FormControl, FormLabel } from '@invoke-ai/ui-library'; +import { useAppSelector } from 'app/store/storeHooks'; +import { InformationalPopover } from 'common/components/InformationalPopover/InformationalPopover'; +import { useCallback, useMemo } from 'react'; +import type { UseControllerProps } from 'react-hook-form'; +import { useController } from 'react-hook-form'; +import { useTranslation } from 'react-i18next'; + +import type { DefaultSettingsFormData } from './DefaultSettingsForm'; + +type DefaultSteps = DefaultSettingsFormData['steps']; + +export function DefaultSteps(props: UseControllerProps) { + const { field } = useController(props); + + const sliderMin = useAppSelector((s) => s.config.sd.steps.sliderMin); + const sliderMax = useAppSelector((s) => s.config.sd.steps.sliderMax); + const numberInputMin = useAppSelector((s) => s.config.sd.steps.numberInputMin); + const numberInputMax = useAppSelector((s) => s.config.sd.steps.numberInputMax); + const coarseStep = useAppSelector((s) => s.config.sd.steps.coarseStep); + const fineStep = useAppSelector((s) => s.config.sd.steps.fineStep); + const { t } = useTranslation(); + const marks = useMemo(() => [sliderMin, Math.floor(sliderMax / 2), sliderMax], [sliderMax, sliderMin]); + + const onChange = useCallback( + (v: number) => { + const updatedValue = { + ...(field.value as DefaultSteps), + value: v, + }; + field.onChange(updatedValue); + }, + [field] + ); + + const value = useMemo(() => { + return (field.value as DefaultSteps).value; + }, [field.value]); + + const isDisabled = useMemo(() => { + return !(field.value as DefaultSteps).isEnabled; + }, [field.value]); + + return ( + + + {t('parameters.steps')} + + + + + + + ); +} diff --git a/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelPanel/DefaultSettings/DefaultVae.tsx b/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelPanel/DefaultSettings/DefaultVae.tsx new file mode 100644 index 0000000000..b32f17dca1 --- /dev/null +++ b/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelPanel/DefaultSettings/DefaultVae.tsx @@ -0,0 +1,65 @@ +import type { ComboboxOnChange } from '@invoke-ai/ui-library'; +import { Combobox, FormControl, FormLabel } from '@invoke-ai/ui-library'; +import { skipToken } from '@reduxjs/toolkit/query'; +import { useAppSelector } from 'app/store/storeHooks'; +import { InformationalPopover } from 'common/components/InformationalPopover/InformationalPopover'; +import { map } from 'lodash-es'; +import { useCallback, useMemo } from 'react'; +import type { UseControllerProps } from 'react-hook-form'; +import { useController } from 'react-hook-form'; +import { useTranslation } from 'react-i18next'; +import { useGetModelConfigQuery, useGetVaeModelsQuery } from 'services/api/endpoints/models'; + +import type { DefaultSettingsFormData } from './DefaultSettingsForm'; + +type DefaultVaeType = DefaultSettingsFormData['vae']; + +export function DefaultVae(props: UseControllerProps) { + const { t } = useTranslation(); + const { field } = useController(props); + const selectedModelKey = useAppSelector((s) => s.modelmanagerV2.selectedModelKey); + const { data: modelData } = useGetModelConfigQuery(selectedModelKey ?? skipToken); + + const { compatibleOptions } = useGetVaeModelsQuery(undefined, { + selectFromResult: ({ data }) => { + const modelArray = map(data?.entities); + const compatibleOptions = modelArray + .filter((vae) => vae.base === modelData?.base) + .map((vae) => ({ label: vae.name, value: vae.key })); + + const defaultOption = { label: 'Default VAE', value: 'default' }; + + return { compatibleOptions: [defaultOption, ...compatibleOptions] }; + }, + }); + + const onChange = useCallback( + (v) => { + const newValue = !v?.value ? 'default' : v.value; + + const updatedValue = { + ...(field.value as DefaultVaeType), + value: newValue, + }; + field.onChange(updatedValue); + }, + [field] + ); + + const value = useMemo(() => { + return compatibleOptions.find((vae) => vae.value === (field.value as DefaultVaeType).value); + }, [compatibleOptions, field.value]); + + const isDisabled = useMemo(() => { + return !(field.value as DefaultVaeType).isEnabled; + }, [field.value]); + + return ( + + + {t('modelManager.vae')} + + + + ); +} diff --git a/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelPanel/DefaultSettings/DefaultVaePrecision.tsx b/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelPanel/DefaultSettings/DefaultVaePrecision.tsx new file mode 100644 index 0000000000..240342b446 --- /dev/null +++ b/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelPanel/DefaultSettings/DefaultVaePrecision.tsx @@ -0,0 +1,51 @@ +import type { ComboboxOnChange } from '@invoke-ai/ui-library'; +import { Combobox, FormControl, FormLabel } from '@invoke-ai/ui-library'; +import { InformationalPopover } from 'common/components/InformationalPopover/InformationalPopover'; +import { isParameterPrecision } from 'features/parameters/types/parameterSchemas'; +import { useCallback, useMemo } from 'react'; +import type { UseControllerProps } from 'react-hook-form'; +import { useController } from 'react-hook-form'; +import { useTranslation } from 'react-i18next'; + +import type { DefaultSettingsFormData } from './DefaultSettingsForm'; + +const options = [ + { label: 'FP16', value: 'fp16' }, + { label: 'FP32', value: 'fp32' }, +]; + +type DefaultVaePrecisionType = DefaultSettingsFormData['vaePrecision']; + +export function DefaultVaePrecision(props: UseControllerProps) { + const { t } = useTranslation(); + const { field } = useController(props); + + const onChange = useCallback( + (v) => { + if (!isParameterPrecision(v?.value)) { + return; + } + const updatedValue = { + ...(field.value as DefaultVaePrecisionType), + value: v.value, + }; + field.onChange(updatedValue); + }, + [field] + ); + + const value = useMemo(() => options.find((o) => o.value === (field.value as DefaultVaePrecisionType).value), [field]); + + const isDisabled = useMemo(() => { + return !(field.value as DefaultVaePrecisionType).isEnabled; + }, [field.value]); + + return ( + + + {t('modelManager.vaePrecision')} + + + + ); +} diff --git a/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelPanel/DefaultSettings/SettingToggle.tsx b/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelPanel/DefaultSettings/SettingToggle.tsx new file mode 100644 index 0000000000..bcea4959a8 --- /dev/null +++ b/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelPanel/DefaultSettings/SettingToggle.tsx @@ -0,0 +1,28 @@ +import { Switch } from '@invoke-ai/ui-library'; +import type { ChangeEvent } from 'react'; +import { useCallback, useMemo } from 'react'; +import type { UseControllerProps } from 'react-hook-form'; +import { useController } from 'react-hook-form'; + +import type { DefaultSettingsFormData, FormField } from './DefaultSettingsForm'; + +export function SettingToggle(props: UseControllerProps) { + const { field } = useController(props); + + const value = useMemo(() => { + return !!(field.value as FormField).isEnabled; + }, [field.value]); + + const onChange = useCallback( + (e: ChangeEvent) => { + const updatedValue: FormField = { + ...(field.value as FormField), + isEnabled: e.target.checked, + }; + field.onChange(updatedValue); + }, + [field] + ); + + return ; +} diff --git a/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelPanel/Metadata/ModelMetadata.tsx b/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelPanel/Metadata/ModelMetadata.tsx new file mode 100644 index 0000000000..7dc3c0bf62 --- /dev/null +++ b/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelPanel/Metadata/ModelMetadata.tsx @@ -0,0 +1,18 @@ +import { Flex } from '@invoke-ai/ui-library'; +import { skipToken } from '@reduxjs/toolkit/query'; +import { useAppSelector } from 'app/store/storeHooks'; +import DataViewer from 'features/gallery/components/ImageMetadataViewer/DataViewer'; +import { useGetModelMetadataQuery } from 'services/api/endpoints/models'; + +export const ModelMetadata = () => { + const selectedModelKey = useAppSelector((s) => s.modelmanagerV2.selectedModelKey); + const { data: metadata } = useGetModelMetadataQuery(selectedModelKey ?? skipToken); + + return ( + <> + + + + + ); +}; diff --git a/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelPanel/Model.tsx b/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelPanel/Model.tsx index 6db804cccf..96e2629443 100644 --- a/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelPanel/Model.tsx +++ b/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelPanel/Model.tsx @@ -1,9 +1,58 @@ +import { Box, Flex, Heading, Tab, TabList, TabPanel, TabPanels, Tabs, Text } from '@invoke-ai/ui-library'; +import { skipToken } from '@reduxjs/toolkit/query'; import { useAppSelector } from 'app/store/storeHooks'; +import { useTranslation } from 'react-i18next'; +import { useGetModelConfigQuery } from 'services/api/endpoints/models'; +import { ModelMetadata } from './Metadata/ModelMetadata'; +import { ModelAttrView } from './ModelAttrView'; import { ModelEdit } from './ModelEdit'; import { ModelView } from './ModelView'; export const Model = () => { + const { t } = useTranslation(); const selectedModelMode = useAppSelector((s) => s.modelmanagerV2.selectedModelMode); - return selectedModelMode === 'view' ? : ; + const selectedModelKey = useAppSelector((s) => s.modelmanagerV2.selectedModelKey); + const { data, isLoading } = useGetModelConfigQuery(selectedModelKey ?? skipToken); + + if (isLoading) { + return {t('common.loading')}; + } + + if (!data) { + return {t('common.somethingWentWrong')}; + } + + return ( + <> + + + {data.name} + + + {data.source && ( + + {t('modelManager.source')}: {data?.source} + + )} + + + + + + + + {t('modelManager.settings')} + {t('modelManager.metadata')} + + + + {selectedModelMode === 'view' ? : } + + + + + + + ); }; diff --git a/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelPanel/ModelView.tsx b/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelPanel/ModelView.tsx index 2acbfe8b3e..0b25e5fdc7 100644 --- a/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelPanel/ModelView.tsx +++ b/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelPanel/ModelView.tsx @@ -1,12 +1,11 @@ -import { Box, Button, Flex, Heading, Text } from '@invoke-ai/ui-library'; +import { Box, Button, Flex, Text } from '@invoke-ai/ui-library'; import { skipToken } from '@reduxjs/toolkit/query'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; -import DataViewer from 'features/gallery/components/ImageMetadataViewer/DataViewer'; import { setSelectedModelMode } from 'features/modelManagerV2/store/modelManagerV2Slice'; import { useCallback, useMemo } from 'react'; import { useTranslation } from 'react-i18next'; import { IoPencil } from 'react-icons/io5'; -import { useGetModelConfigQuery, useGetModelMetadataQuery } from 'services/api/endpoints/models'; +import { useGetModelConfigQuery } from 'services/api/endpoints/models'; import type { CheckpointModelConfig, ControlNetModelConfig, @@ -18,6 +17,7 @@ import type { VAEModelConfig, } from 'services/api/types'; +import { DefaultSettings } from './DefaultSettings'; import { ModelAttrView } from './ModelAttrView'; import { ModelConvert } from './ModelConvert'; @@ -26,7 +26,6 @@ export const ModelView = () => { const dispatch = useAppDispatch(); const selectedModelKey = useAppSelector((s) => s.modelmanagerV2.selectedModelKey); const { data, isLoading } = useGetModelConfigQuery(selectedModelKey ?? skipToken); - const { data: metadata } = useGetModelMetadataQuery(selectedModelKey ?? skipToken); const modelData = useMemo(() => { if (!data) { @@ -73,85 +72,56 @@ export const ModelView = () => { return {t('common.somethingWentWrong')}; } return ( - - - - - {modelData.name} - - - {modelData.source && ( - - {t('modelManager.source')}: {modelData.source} - - )} - - + + + + {modelData.type === 'main' && modelData.format === 'checkpoint' && } - - - - - - - - {t('modelManager.modelSettings')} - - - - - - - - - - - - {modelData.type === 'main' && ( - <> - - {modelData.format === 'diffusers' && ( - - )} - {modelData.format === 'checkpoint' && ( - - )} - - - - - - - - - - - - - )} - {modelData.type === 'ip_adapter' && ( + + + + + + + + + + {modelData.type === 'main' && ( + <> - - - )} - - - + {modelData.format === 'diffusers' && ( + + )} + {modelData.format === 'checkpoint' && ( + + )} - {metadata && ( - <> - - {t('modelManager.modelMetadata')} - - - - - - )} + + + + + + + + + + + + )} + {modelData.type === 'ip_adapter' && ( + + + + )} + + + + + ); }; diff --git a/invokeai/frontend/web/src/features/parameters/components/MainModel/UseDefaultSettingsButton.tsx b/invokeai/frontend/web/src/features/parameters/components/MainModel/UseDefaultSettingsButton.tsx new file mode 100644 index 0000000000..7b322a3227 --- /dev/null +++ b/invokeai/frontend/web/src/features/parameters/components/MainModel/UseDefaultSettingsButton.tsx @@ -0,0 +1,28 @@ +import { IconButton } from '@invoke-ai/ui-library'; +import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; +import { setDefaultSettings } from 'features/parameters/store/actions'; +import { useCallback } from 'react'; +import { useTranslation } from 'react-i18next'; +import { RiSparklingFill } from 'react-icons/ri'; + +export const UseDefaultSettingsButton = () => { + const model = useAppSelector((s) => s.generation.model); + const { t } = useTranslation(); + const dispatch = useAppDispatch(); + + const handleClickDefaultSettings = useCallback(() => { + dispatch(setDefaultSettings()); + }, [dispatch]); + + return ( + } + tooltip={t('modelManager.useDefaultSettings')} + aria-label={t('modelManager.useDefaultSettings')} + isDisabled={!model} + onClick={handleClickDefaultSettings} + size="sm" + variant="ghost" + /> + ); +}; diff --git a/invokeai/frontend/web/src/features/parameters/store/actions.ts b/invokeai/frontend/web/src/features/parameters/store/actions.ts index f7bf127c05..3b43129720 100644 --- a/invokeai/frontend/web/src/features/parameters/store/actions.ts +++ b/invokeai/frontend/web/src/features/parameters/store/actions.ts @@ -5,3 +5,5 @@ import type { ImageDTO } from 'services/api/types'; export const initialImageSelected = createAction('generation/initialImageSelected'); export const modelSelected = createAction('generation/modelSelected'); + +export const setDefaultSettings = createAction('generation/setDefaultSettings'); diff --git a/invokeai/frontend/web/src/features/parameters/store/generationSlice.ts b/invokeai/frontend/web/src/features/parameters/store/generationSlice.ts index 49ca507439..0f36d8b477 100644 --- a/invokeai/frontend/web/src/features/parameters/store/generationSlice.ts +++ b/invokeai/frontend/web/src/features/parameters/store/generationSlice.ts @@ -230,6 +230,12 @@ export const generationSlice = createSlice({ state.height = optimalDimension; } } + if (action.payload.sd?.scheduler) { + state.scheduler = action.payload.sd.scheduler; + } + if (action.payload.sd?.vaePrecision) { + state.vaePrecision = action.payload.sd.vaePrecision; + } }); // TODO: This is a temp fix to reduce issues with T2I adapter having a different downscaling diff --git a/invokeai/frontend/web/src/features/settingsAccordions/components/GenerationSettingsAccordion/GenerationSettingsAccordion.tsx b/invokeai/frontend/web/src/features/settingsAccordions/components/GenerationSettingsAccordion/GenerationSettingsAccordion.tsx index 1258189f40..ab2d5abed6 100644 --- a/invokeai/frontend/web/src/features/settingsAccordions/components/GenerationSettingsAccordion/GenerationSettingsAccordion.tsx +++ b/invokeai/frontend/web/src/features/settingsAccordions/components/GenerationSettingsAccordion/GenerationSettingsAccordion.tsx @@ -12,6 +12,7 @@ import ParamScheduler from 'features/parameters/components/Core/ParamScheduler'; import ParamSteps from 'features/parameters/components/Core/ParamSteps'; import { NavigateToModelManagerButton } from 'features/parameters/components/MainModel/NavigateToModelManagerButton'; import ParamMainModelSelect from 'features/parameters/components/MainModel/ParamMainModelSelect'; +import { UseDefaultSettingsButton } from 'features/parameters/components/MainModel/UseDefaultSettingsButton'; import { useExpanderToggle } from 'features/settingsAccordions/hooks/useExpanderToggle'; import { useStandaloneAccordionToggle } from 'features/settingsAccordions/hooks/useStandaloneAccordionToggle'; import { filter } from 'lodash-es'; @@ -58,6 +59,7 @@ export const GenerationSettingsAccordion = memo(() => { + diff --git a/invokeai/frontend/web/src/features/system/store/configSlice.ts b/invokeai/frontend/web/src/features/system/store/configSlice.ts index 4e1b734a66..76280df1ce 100644 --- a/invokeai/frontend/web/src/features/system/store/configSlice.ts +++ b/invokeai/frontend/web/src/features/system/store/configSlice.ts @@ -41,6 +41,8 @@ const initialConfigState: AppConfig = { boundingBoxHeight: { ...baseDimensionConfig }, scaledBoundingBoxWidth: { ...baseDimensionConfig }, scaledBoundingBoxHeight: { ...baseDimensionConfig }, + scheduler: 'euler', + vaePrecision: 'fp32', steps: { initial: 30, sliderMin: 1, diff --git a/invokeai/frontend/web/src/services/api/endpoints/models.ts b/invokeai/frontend/web/src/services/api/endpoints/models.ts index 04c65b59f6..dac6594255 100644 --- a/invokeai/frontend/web/src/services/api/endpoints/models.ts +++ b/invokeai/frontend/web/src/services/api/endpoints/models.ts @@ -24,7 +24,15 @@ export type UpdateModelArg = { body: paths['/api/v2/models/i/{key}']['patch']['requestBody']['content']['application/json']; }; +type UpdateModelMetadataArg = { + key: paths['/api/v2/models/i/{key}/metadata']['patch']['parameters']['path']['key']; + body: paths['/api/v2/models/i/{key}/metadata']['patch']['requestBody']['content']['application/json']; +}; + type UpdateModelResponse = paths['/api/v2/models/i/{key}']['patch']['responses']['200']['content']['application/json']; +type UpdateModelMetadataResponse = + paths['/api/v2/models/i/{key}/metadata']['patch']['responses']['200']['content']['application/json']; + type GetModelConfigResponse = paths['/api/v2/models/i/{key}']['get']['responses']['200']['content']['application/json']; type GetModelMetadataResponse = @@ -172,6 +180,16 @@ export const modelsApi = api.injectEndpoints({ }, invalidatesTags: ['Model'], }), + updateModelMetadata: build.mutation({ + query: ({ key, body }) => { + return { + url: buildModelsUrl(`i/${key}/metadata`), + method: 'PATCH', + body: body, + }; + }, + invalidatesTags: ['Model'], + }), installModel: build.mutation({ query: ({ source, config, access_token }) => { return { @@ -351,6 +369,7 @@ export const { useGetModelMetadataQuery, useDeleteModelImportMutation, usePruneModelImportsMutation, + useUpdateModelMetadataMutation, } = modelsApi; const upsertModelConfigs = ( diff --git a/invokeai/frontend/web/src/services/api/schema.ts b/invokeai/frontend/web/src/services/api/schema.ts index 12227d1ae9..560feb93ba 100644 --- a/invokeai/frontend/web/src/services/api/schema.ts +++ b/invokeai/frontend/web/src/services/api/schema.ts @@ -60,6 +60,11 @@ export type paths = { * @description Get a model metadata object. */ get: operations["get_model_metadata"]; + /** + * Update Model Metadata + * @description Updates or creates a model metadata object. + */ + patch: operations["update_model_metadata"]; }; "/api/v2/models/tags": { /** @@ -757,7 +762,14 @@ export type components = { * Tags * @description tags provided by model source */ - tags: string[]; + tags?: string[] | null; + /** + * Trigger Phrases + * @description trigger phrases for this model + */ + trigger_phrases?: string[] | null; + /** @description default settings for this model */ + default_settings?: components["schemas"]["ModelDefaultSettings"] | null; /** * Type * @default basemetadata @@ -1806,7 +1818,14 @@ export type components = { * Tags * @description tags provided by model source */ - tags: string[]; + tags?: string[] | null; + /** + * Trigger Phrases + * @description trigger phrases for this model + */ + trigger_phrases?: string[] | null; + /** @description default settings for this model */ + default_settings?: components["schemas"]["ModelDefaultSettings"] | null; /** * Files * @description model files and their sizes @@ -4264,7 +4283,7 @@ export type components = { * @description The nodes in this graph */ nodes: { - [key: string]: components["schemas"]["ColorInvocation"] | components["schemas"]["CoreMetadataInvocation"] | components["schemas"]["ImageCropInvocation"] | components["schemas"]["CalculateImageTilesInvocation"] | components["schemas"]["RandomIntInvocation"] | components["schemas"]["MediapipeFaceProcessorInvocation"] | components["schemas"]["SDXLCompelPromptInvocation"] | components["schemas"]["FloatToIntegerInvocation"] | components["schemas"]["FaceIdentifierInvocation"] | components["schemas"]["FreeUInvocation"] | components["schemas"]["ImageMultiplyInvocation"] | components["schemas"]["StepParamEasingInvocation"] | components["schemas"]["SDXLModelLoaderInvocation"] | components["schemas"]["SDXLLoraLoaderInvocation"] | components["schemas"]["RandomFloatInvocation"] | components["schemas"]["ImagePasteInvocation"] | components["schemas"]["ResizeLatentsInvocation"] | components["schemas"]["SaveImageInvocation"] | components["schemas"]["StringJoinInvocation"] | components["schemas"]["CenterPadCropInvocation"] | components["schemas"]["ColorCorrectInvocation"] | components["schemas"]["StringJoinThreeInvocation"] | components["schemas"]["IPAdapterInvocation"] | components["schemas"]["NormalbaeImageProcessorInvocation"] | components["schemas"]["CompelInvocation"] | components["schemas"]["ImageHueAdjustmentInvocation"] | components["schemas"]["LatentsCollectionInvocation"] | components["schemas"]["BlendLatentsInvocation"] | components["schemas"]["PidiImageProcessorInvocation"] | components["schemas"]["ImageConvertInvocation"] | components["schemas"]["IdealSizeInvocation"] | components["schemas"]["FloatMathInvocation"] | components["schemas"]["StringReplaceInvocation"] | components["schemas"]["ImageCollectionInvocation"] | components["schemas"]["InfillTileInvocation"] | components["schemas"]["PromptsFromFileInvocation"] | components["schemas"]["FloatLinearRangeInvocation"] | components["schemas"]["CropLatentsCoreInvocation"] | components["schemas"]["LeresImageProcessorInvocation"] | components["schemas"]["ShowImageInvocation"] | components["schemas"]["ImageChannelInvocation"] | components["schemas"]["SDXLRefinerCompelPromptInvocation"] | components["schemas"]["UnsharpMaskInvocation"] | components["schemas"]["CalculateImageTilesMinimumOverlapInvocation"] | components["schemas"]["MaskCombineInvocation"] | components["schemas"]["BooleanCollectionInvocation"] | components["schemas"]["IntegerInvocation"] | components["schemas"]["FloatInvocation"] | components["schemas"]["ColorMapImageProcessorInvocation"] | components["schemas"]["ImageWatermarkInvocation"] | components["schemas"]["ImageLerpInvocation"] | components["schemas"]["MaskEdgeInvocation"] | components["schemas"]["ControlNetInvocation"] | components["schemas"]["CannyImageProcessorInvocation"] | components["schemas"]["IntegerMathInvocation"] | components["schemas"]["SeamlessModeInvocation"] | components["schemas"]["StringSplitNegInvocation"] | components["schemas"]["MidasDepthImageProcessorInvocation"] | components["schemas"]["RangeInvocation"] | components["schemas"]["ESRGANInvocation"] | components["schemas"]["TileToPropertiesInvocation"] | components["schemas"]["ImageScaleInvocation"] | components["schemas"]["BlankImageInvocation"] | components["schemas"]["MergeMetadataInvocation"] | components["schemas"]["LaMaInfillInvocation"] | components["schemas"]["ImageNSFWBlurInvocation"] | components["schemas"]["DenoiseLatentsInvocation"] | components["schemas"]["MetadataItemInvocation"] | components["schemas"]["ImageChannelMultiplyInvocation"] | components["schemas"]["CanvasPasteBackInvocation"] | components["schemas"]["StringCollectionInvocation"] | components["schemas"]["LoraLoaderInvocation"] | components["schemas"]["SchedulerInvocation"] | components["schemas"]["MaskFromAlphaInvocation"] | components["schemas"]["ConditioningInvocation"] | components["schemas"]["LatentsToImageInvocation"] | components["schemas"]["MetadataInvocation"] | components["schemas"]["CollectInvocation"] | components["schemas"]["DWOpenposeImageProcessorInvocation"] | components["schemas"]["ZoeDepthImageProcessorInvocation"] | components["schemas"]["CreateDenoiseMaskInvocation"] | components["schemas"]["ScaleLatentsInvocation"] | components["schemas"]["RangeOfSizeInvocation"] | components["schemas"]["ClipSkipInvocation"] | components["schemas"]["InfillPatchMatchInvocation"] | components["schemas"]["CvInpaintInvocation"] | components["schemas"]["FaceOffInvocation"] | components["schemas"]["StringSplitInvocation"] | components["schemas"]["ImageResizeInvocation"] | components["schemas"]["MlsdImageProcessorInvocation"] | components["schemas"]["CalculateImageTilesEvenSplitInvocation"] | components["schemas"]["LineartImageProcessorInvocation"] | components["schemas"]["ImageBlurInvocation"] | components["schemas"]["AddInvocation"] | components["schemas"]["SDXLRefinerModelLoaderInvocation"] | components["schemas"]["ImageChannelOffsetInvocation"] | components["schemas"]["VaeLoaderInvocation"] | components["schemas"]["ContentShuffleImageProcessorInvocation"] | components["schemas"]["RandomRangeInvocation"] | components["schemas"]["CV2InfillInvocation"] | components["schemas"]["ImageInverseLerpInvocation"] | components["schemas"]["MultiplyInvocation"] | components["schemas"]["PairTileImageInvocation"] | components["schemas"]["DivideInvocation"] | components["schemas"]["DynamicPromptInvocation"] | components["schemas"]["CreateGradientMaskInvocation"] | components["schemas"]["InfillColorInvocation"] | components["schemas"]["TileResamplerProcessorInvocation"] | components["schemas"]["ImageInvocation"] | components["schemas"]["HedImageProcessorInvocation"] | components["schemas"]["BooleanInvocation"] | components["schemas"]["LatentsInvocation"] | components["schemas"]["DepthAnythingImageProcessorInvocation"] | components["schemas"]["IterateInvocation"] | components["schemas"]["RoundInvocation"] | components["schemas"]["FloatCollectionInvocation"] | components["schemas"]["MainModelLoaderInvocation"] | components["schemas"]["SubtractInvocation"] | components["schemas"]["ImageToLatentsInvocation"] | components["schemas"]["LineartAnimeImageProcessorInvocation"] | components["schemas"]["NoiseInvocation"] | components["schemas"]["T2IAdapterInvocation"] | components["schemas"]["ConditioningCollectionInvocation"] | components["schemas"]["IntegerCollectionInvocation"] | components["schemas"]["FaceMaskInvocation"] | components["schemas"]["SegmentAnythingProcessorInvocation"] | components["schemas"]["StringInvocation"] | components["schemas"]["MergeTilesToImageInvocation"]; + [key: string]: components["schemas"]["SeamlessModeInvocation"] | components["schemas"]["MergeMetadataInvocation"] | components["schemas"]["ColorCorrectInvocation"] | components["schemas"]["LatentsToImageInvocation"] | components["schemas"]["MaskEdgeInvocation"] | components["schemas"]["PidiImageProcessorInvocation"] | components["schemas"]["FreeUInvocation"] | components["schemas"]["MergeTilesToImageInvocation"] | components["schemas"]["CanvasPasteBackInvocation"] | components["schemas"]["IntegerMathInvocation"] | components["schemas"]["ImageCropInvocation"] | components["schemas"]["ImageMultiplyInvocation"] | components["schemas"]["LineartImageProcessorInvocation"] | components["schemas"]["ScaleLatentsInvocation"] | components["schemas"]["FaceMaskInvocation"] | components["schemas"]["ESRGANInvocation"] | components["schemas"]["BooleanInvocation"] | components["schemas"]["FloatMathInvocation"] | components["schemas"]["VaeLoaderInvocation"] | components["schemas"]["MetadataItemInvocation"] | components["schemas"]["FloatLinearRangeInvocation"] | components["schemas"]["LatentsCollectionInvocation"] | components["schemas"]["NormalbaeImageProcessorInvocation"] | components["schemas"]["IdealSizeInvocation"] | components["schemas"]["CvInpaintInvocation"] | components["schemas"]["StringCollectionInvocation"] | components["schemas"]["ContentShuffleImageProcessorInvocation"] | components["schemas"]["StringJoinThreeInvocation"] | components["schemas"]["ColorMapImageProcessorInvocation"] | components["schemas"]["FaceOffInvocation"] | components["schemas"]["UnsharpMaskInvocation"] | components["schemas"]["FloatCollectionInvocation"] | components["schemas"]["MidasDepthImageProcessorInvocation"] | components["schemas"]["ImageChannelOffsetInvocation"] | components["schemas"]["InfillPatchMatchInvocation"] | components["schemas"]["DynamicPromptInvocation"] | components["schemas"]["CannyImageProcessorInvocation"] | components["schemas"]["InfillColorInvocation"] | components["schemas"]["NoiseInvocation"] | components["schemas"]["ImageToLatentsInvocation"] | components["schemas"]["InfillTileInvocation"] | components["schemas"]["LatentsInvocation"] | components["schemas"]["ConditioningInvocation"] | components["schemas"]["DepthAnythingImageProcessorInvocation"] | components["schemas"]["MultiplyInvocation"] | components["schemas"]["LoraLoaderInvocation"] | components["schemas"]["StringReplaceInvocation"] | components["schemas"]["CompelInvocation"] | components["schemas"]["CalculateImageTilesMinimumOverlapInvocation"] | components["schemas"]["IntegerInvocation"] | components["schemas"]["ResizeLatentsInvocation"] | components["schemas"]["LeresImageProcessorInvocation"] | components["schemas"]["SDXLModelLoaderInvocation"] | components["schemas"]["ImageWatermarkInvocation"] | components["schemas"]["CreateGradientMaskInvocation"] | components["schemas"]["RangeInvocation"] | components["schemas"]["ImagePasteInvocation"] | components["schemas"]["SDXLRefinerModelLoaderInvocation"] | components["schemas"]["ImageScaleInvocation"] | components["schemas"]["ZoeDepthImageProcessorInvocation"] | components["schemas"]["FloatToIntegerInvocation"] | components["schemas"]["PairTileImageInvocation"] | components["schemas"]["T2IAdapterInvocation"] | components["schemas"]["BooleanCollectionInvocation"] | components["schemas"]["RandomRangeInvocation"] | components["schemas"]["RoundInvocation"] | components["schemas"]["PromptsFromFileInvocation"] | components["schemas"]["LaMaInfillInvocation"] | components["schemas"]["StepParamEasingInvocation"] | components["schemas"]["CenterPadCropInvocation"] | components["schemas"]["CropLatentsCoreInvocation"] | components["schemas"]["SubtractInvocation"] | components["schemas"]["MaskCombineInvocation"] | components["schemas"]["IntegerCollectionInvocation"] | components["schemas"]["ControlNetInvocation"] | components["schemas"]["SDXLLoraLoaderInvocation"] | components["schemas"]["TileResamplerProcessorInvocation"] | components["schemas"]["CV2InfillInvocation"] | components["schemas"]["CollectInvocation"] | components["schemas"]["ImageInvocation"] | components["schemas"]["DWOpenposeImageProcessorInvocation"] | components["schemas"]["FaceIdentifierInvocation"] | components["schemas"]["ImageBlurInvocation"] | components["schemas"]["ImageChannelMultiplyInvocation"] | components["schemas"]["AddInvocation"] | components["schemas"]["SDXLCompelPromptInvocation"] | components["schemas"]["IterateInvocation"] | components["schemas"]["CalculateImageTilesInvocation"] | components["schemas"]["SDXLRefinerCompelPromptInvocation"] | components["schemas"]["ImageResizeInvocation"] | components["schemas"]["SchedulerInvocation"] | components["schemas"]["ImageCollectionInvocation"] | components["schemas"]["ShowImageInvocation"] | components["schemas"]["RandomFloatInvocation"] | components["schemas"]["StringSplitNegInvocation"] | components["schemas"]["StringSplitInvocation"] | components["schemas"]["ColorInvocation"] | components["schemas"]["ImageConvertInvocation"] | components["schemas"]["BlendLatentsInvocation"] | components["schemas"]["MediapipeFaceProcessorInvocation"] | components["schemas"]["CalculateImageTilesEvenSplitInvocation"] | components["schemas"]["CreateDenoiseMaskInvocation"] | components["schemas"]["ConditioningCollectionInvocation"] | components["schemas"]["SegmentAnythingProcessorInvocation"] | components["schemas"]["StringInvocation"] | components["schemas"]["DenoiseLatentsInvocation"] | components["schemas"]["RangeOfSizeInvocation"] | components["schemas"]["DivideInvocation"] | components["schemas"]["ImageLerpInvocation"] | components["schemas"]["FloatInvocation"] | components["schemas"]["MainModelLoaderInvocation"] | components["schemas"]["BlankImageInvocation"] | components["schemas"]["StringJoinInvocation"] | components["schemas"]["ClipSkipInvocation"] | components["schemas"]["ImageChannelInvocation"] | components["schemas"]["TileToPropertiesInvocation"] | components["schemas"]["ImageHueAdjustmentInvocation"] | components["schemas"]["HedImageProcessorInvocation"] | components["schemas"]["MetadataInvocation"] | components["schemas"]["IPAdapterInvocation"] | components["schemas"]["RandomIntInvocation"] | components["schemas"]["MaskFromAlphaInvocation"] | components["schemas"]["ImageNSFWBlurInvocation"] | components["schemas"]["MlsdImageProcessorInvocation"] | components["schemas"]["ImageInverseLerpInvocation"] | components["schemas"]["SaveImageInvocation"] | components["schemas"]["LineartAnimeImageProcessorInvocation"] | components["schemas"]["CoreMetadataInvocation"]; }; /** * Edges @@ -4301,7 +4320,7 @@ export type components = { * @description The results of node executions */ results: { - [key: string]: components["schemas"]["IntegerCollectionOutput"] | components["schemas"]["IterateInvocationOutput"] | components["schemas"]["StringOutput"] | components["schemas"]["ConditioningOutput"] | components["schemas"]["SDXLLoraLoaderOutput"] | components["schemas"]["LoraLoaderOutput"] | components["schemas"]["ModelLoaderOutput"] | components["schemas"]["IntegerOutput"] | components["schemas"]["CalculateImageTilesOutput"] | components["schemas"]["ImageCollectionOutput"] | components["schemas"]["TileToPropertiesOutput"] | components["schemas"]["StringPosNegOutput"] | components["schemas"]["BooleanCollectionOutput"] | components["schemas"]["DenoiseMaskOutput"] | components["schemas"]["UNetOutput"] | components["schemas"]["IPAdapterOutput"] | components["schemas"]["SchedulerOutput"] | components["schemas"]["FloatOutput"] | components["schemas"]["String2Output"] | components["schemas"]["SeamlessModeOutput"] | components["schemas"]["NoiseOutput"] | components["schemas"]["SDXLModelLoaderOutput"] | components["schemas"]["MetadataItemOutput"] | components["schemas"]["ImageOutput"] | components["schemas"]["ColorOutput"] | components["schemas"]["IdealSizeOutput"] | components["schemas"]["ColorCollectionOutput"] | components["schemas"]["ControlOutput"] | components["schemas"]["SDXLRefinerModelLoaderOutput"] | components["schemas"]["BooleanOutput"] | components["schemas"]["MetadataOutput"] | components["schemas"]["CLIPOutput"] | components["schemas"]["ConditioningCollectionOutput"] | components["schemas"]["LatentsCollectionOutput"] | components["schemas"]["T2IAdapterOutput"] | components["schemas"]["VAEOutput"] | components["schemas"]["FaceMaskOutput"] | components["schemas"]["PairTileImageOutput"] | components["schemas"]["FaceOffOutput"] | components["schemas"]["ClipSkipInvocationOutput"] | components["schemas"]["CollectInvocationOutput"] | components["schemas"]["StringCollectionOutput"] | components["schemas"]["LatentsOutput"] | components["schemas"]["FloatCollectionOutput"]; + [key: string]: components["schemas"]["BooleanOutput"] | components["schemas"]["SDXLRefinerModelLoaderOutput"] | components["schemas"]["PairTileImageOutput"] | components["schemas"]["ColorCollectionOutput"] | components["schemas"]["IdealSizeOutput"] | components["schemas"]["String2Output"] | components["schemas"]["FloatCollectionOutput"] | components["schemas"]["ColorOutput"] | components["schemas"]["IPAdapterOutput"] | components["schemas"]["MetadataItemOutput"] | components["schemas"]["ConditioningCollectionOutput"] | components["schemas"]["NoiseOutput"] | components["schemas"]["ClipSkipInvocationOutput"] | components["schemas"]["ImageOutput"] | components["schemas"]["VAEOutput"] | components["schemas"]["MetadataOutput"] | components["schemas"]["StringCollectionOutput"] | components["schemas"]["ControlOutput"] | components["schemas"]["FaceOffOutput"] | components["schemas"]["StringOutput"] | components["schemas"]["SchedulerOutput"] | components["schemas"]["ImageCollectionOutput"] | components["schemas"]["TileToPropertiesOutput"] | components["schemas"]["LoraLoaderOutput"] | components["schemas"]["StringPosNegOutput"] | components["schemas"]["CollectInvocationOutput"] | components["schemas"]["ModelLoaderOutput"] | components["schemas"]["DenoiseMaskOutput"] | components["schemas"]["ConditioningOutput"] | components["schemas"]["CalculateImageTilesOutput"] | components["schemas"]["FaceMaskOutput"] | components["schemas"]["IntegerCollectionOutput"] | components["schemas"]["BooleanCollectionOutput"] | components["schemas"]["IntegerOutput"] | components["schemas"]["FloatOutput"] | components["schemas"]["CLIPOutput"] | components["schemas"]["SeamlessModeOutput"] | components["schemas"]["T2IAdapterOutput"] | components["schemas"]["UNetOutput"] | components["schemas"]["IterateInvocationOutput"] | components["schemas"]["SDXLModelLoaderOutput"] | components["schemas"]["LatentsOutput"] | components["schemas"]["SDXLLoraLoaderOutput"] | components["schemas"]["LatentsCollectionOutput"]; }; /** * Errors @@ -4424,7 +4443,14 @@ export type components = { * Tags * @description tags provided by model source */ - tags: string[]; + tags?: string[] | null; + /** + * Trigger Phrases + * @description trigger phrases for this model + */ + trigger_phrases?: string[] | null; + /** @description default settings for this model */ + default_settings?: components["schemas"]["ModelDefaultSettings"] | null; /** * Files * @description model files and their sizes @@ -7430,6 +7456,21 @@ export type components = { */ type: "mlsd_image_processor"; }; + /** ModelDefaultSettings */ + ModelDefaultSettings: { + /** Vae */ + vae: string | null; + /** Vae Precision */ + vae_precision: string | null; + /** Scheduler */ + scheduler: ("ddim" | "ddpm" | "deis" | "lms" | "lms_k" | "pndm" | "heun" | "heun_k" | "euler" | "euler_k" | "euler_a" | "kdpm_2" | "kdpm_2_a" | "dpmpp_2s" | "dpmpp_2s_k" | "dpmpp_2m" | "dpmpp_2m_k" | "dpmpp_2m_sde" | "dpmpp_2m_sde_k" | "dpmpp_sde" | "dpmpp_sde_k" | "unipc" | "lcm") | null; + /** Steps */ + steps: number | null; + /** Cfg Scale */ + cfg_scale: number | null; + /** Cfg Rescale Multiplier */ + cfg_rescale_multiplier: number | null; + }; /** * ModelFormat * @description Storage format of model. @@ -7556,6 +7597,24 @@ export type components = { */ unet: components["schemas"]["UNetField"]; }; + /** + * ModelMetadataChanges + * @description A set of changes to apply to model metadata. + * + * Only limited changes are valid: + * - `trigger_phrases`: the list of trigger phrases for this model + * - `default_settings`: the user-configured default settings for this model + */ + ModelMetadataChanges: { + /** + * Trigger Phrases + * @description The model's list of trigger phrases + */ + trigger_phrases?: string[] | null; + /** @description The user-configured default settings for this model */ + default_settings?: components["schemas"]["ModelDefaultSettings"] | null; + [key: string]: unknown; + }; /** * ModelRecordOrderBy * @description The order in which to return model summaries. @@ -11203,6 +11262,47 @@ export type operations = { }; }; }; + /** + * Update Model Metadata + * @description Updates or creates a model metadata object. + */ + update_model_metadata: { + parameters: { + path: { + /** @description Key of the model repo metadata to fetch. */ + key: string; + }; + }; + requestBody: { + content: { + "application/json": components["schemas"]["ModelMetadataChanges"]; + }; + }; + responses: { + /** @description Successful Response */ + 200: { + content: { + "application/json": (components["schemas"]["BaseMetadata"] | components["schemas"]["HuggingFaceMetadata"] | components["schemas"]["CivitaiMetadata"]) | null; + }; + }; + /** @description The model metadata was updated successfully */ + 201: { + content: { + "application/json": unknown; + }; + }; + /** @description Bad request */ + 400: { + content: never; + }; + /** @description Validation Error */ + 422: { + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; /** * List Tags * @description Get a unique set of all the model tags.