mirror of
https://github.com/invoke-ai/InvokeAI
synced 2024-08-30 20:32:17 +00:00
Default model settings (#5850)
* UI in MM to create trigger phrases * add scheduler and vaePrecision to config * UI for configuring default settings for models' * hook MM default model settings up to API * add button to set default settings in parameters * pull out trigger phrases * back-end for default settings * lint * remove log; gi * ruff * ruff format --------- Co-authored-by: Mary Hipp <maryhipp@Marys-MacBook-Air.local>
This commit is contained in:
parent
e2448d2b4e
commit
644232f7ea
@ -14,6 +14,7 @@ from starlette.exceptions import HTTPException
|
|||||||
from typing_extensions import Annotated
|
from typing_extensions import Annotated
|
||||||
|
|
||||||
from invokeai.app.services.model_install import ModelInstallJob
|
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 (
|
from invokeai.app.services.model_records import (
|
||||||
DuplicateModelException,
|
DuplicateModelException,
|
||||||
InvalidModelException,
|
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.merge import MergeInterpolationMethod, ModelMerger
|
||||||
from invokeai.backend.model_manager.metadata import AnyModelRepoMetadata
|
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 invokeai.backend.model_manager.search import ModelSearch
|
||||||
|
|
||||||
from ..dependencies import ApiDependencies
|
from ..dependencies import ApiDependencies
|
||||||
@ -243,6 +245,47 @@ async def get_model_metadata(
|
|||||||
return result
|
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(
|
@model_manager_router.get(
|
||||||
"/tags",
|
"/tags",
|
||||||
operation_id="list_tags",
|
operation_id="list_tags",
|
||||||
|
@ -4,9 +4,25 @@ Storage for Model Metadata
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
from abc import ABC, abstractmethod
|
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 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):
|
class ModelMetadataStoreBase(ABC):
|
||||||
|
@ -179,8 +179,9 @@ class ModelMetadataStoreSQL(ModelMetadataStoreBase):
|
|||||||
)
|
)
|
||||||
return {x[0] for x in self._cursor.fetchall()}
|
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."""
|
"""Update tags for the model referenced by model_key."""
|
||||||
|
if tags:
|
||||||
# remove previous tags from this model
|
# remove previous tags from this model
|
||||||
self._cursor.execute(
|
self._cursor.execute(
|
||||||
"""--sql
|
"""--sql
|
||||||
|
@ -25,6 +25,7 @@ from pydantic.networks import AnyHttpUrl
|
|||||||
from requests.sessions import Session
|
from requests.sessions import Session
|
||||||
from typing_extensions import Annotated
|
from typing_extensions import Annotated
|
||||||
|
|
||||||
|
from invokeai.app.invocations.constants import SCHEDULER_NAME_VALUES
|
||||||
from invokeai.backend.model_manager import ModelRepoVariant
|
from invokeai.backend.model_manager import ModelRepoVariant
|
||||||
|
|
||||||
from ..util import select_hf_files
|
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)
|
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):
|
class ModelMetadataBase(BaseModel):
|
||||||
"""Base class for model metadata information."""
|
"""Base class for model metadata information."""
|
||||||
|
|
||||||
name: str = Field(description="model's name")
|
name: str = Field(description="model's name")
|
||||||
author: str = Field(description="model's author")
|
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):
|
class BaseMetadata(ModelMetadataBase):
|
||||||
|
@ -78,6 +78,7 @@
|
|||||||
"aboutDesc": "Using Invoke for work? Check out:",
|
"aboutDesc": "Using Invoke for work? Check out:",
|
||||||
"aboutHeading": "Own Your Creative Power",
|
"aboutHeading": "Own Your Creative Power",
|
||||||
"accept": "Accept",
|
"accept": "Accept",
|
||||||
|
"add": "Add",
|
||||||
"advanced": "Advanced",
|
"advanced": "Advanced",
|
||||||
"advancedOptions": "Advanced Options",
|
"advancedOptions": "Advanced Options",
|
||||||
"ai": "ai",
|
"ai": "ai",
|
||||||
@ -734,6 +735,8 @@
|
|||||||
"customConfig": "Custom Config",
|
"customConfig": "Custom Config",
|
||||||
"customConfigFileLocation": "Custom Config File Location",
|
"customConfigFileLocation": "Custom Config File Location",
|
||||||
"customSaveLocation": "Custom Save Location",
|
"customSaveLocation": "Custom Save Location",
|
||||||
|
"defaultSettings": "Default Settings",
|
||||||
|
"defaultSettingsSaved": "Default Settings Saved",
|
||||||
"delete": "Delete",
|
"delete": "Delete",
|
||||||
"deleteConfig": "Delete Config",
|
"deleteConfig": "Delete Config",
|
||||||
"deleteModel": "Delete Model",
|
"deleteModel": "Delete Model",
|
||||||
@ -768,6 +771,7 @@
|
|||||||
"mergedModelName": "Merged Model Name",
|
"mergedModelName": "Merged Model Name",
|
||||||
"mergedModelSaveLocation": "Save Location",
|
"mergedModelSaveLocation": "Save Location",
|
||||||
"mergeModels": "Merge Models",
|
"mergeModels": "Merge Models",
|
||||||
|
"metadata": "Metadata",
|
||||||
"model": "Model",
|
"model": "Model",
|
||||||
"modelAdded": "Model Added",
|
"modelAdded": "Model Added",
|
||||||
"modelConversionFailed": "Model Conversion Failed",
|
"modelConversionFailed": "Model Conversion Failed",
|
||||||
@ -839,9 +843,12 @@
|
|||||||
"statusConverting": "Converting",
|
"statusConverting": "Converting",
|
||||||
"syncModels": "Sync Models",
|
"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.",
|
"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",
|
"upcastAttention": "Upcast Attention",
|
||||||
"updateModel": "Update Model",
|
"updateModel": "Update Model",
|
||||||
"useCustomConfig": "Use Custom Config",
|
"useCustomConfig": "Use Custom Config",
|
||||||
|
"useDefaultSettings": "Use Default Settings",
|
||||||
"v1": "v1",
|
"v1": "v1",
|
||||||
"v2_768": "v2 (768px)",
|
"v2_768": "v2 (768px)",
|
||||||
"v2_base": "v2 (512px)",
|
"v2_base": "v2 (512px)",
|
||||||
|
@ -55,6 +55,8 @@ import { addUpscaleRequestedListener } from 'app/store/middleware/listenerMiddle
|
|||||||
import { addWorkflowLoadRequestedListener } from 'app/store/middleware/listenerMiddleware/listeners/workflowLoadRequested';
|
import { addWorkflowLoadRequestedListener } from 'app/store/middleware/listenerMiddleware/listeners/workflowLoadRequested';
|
||||||
import type { AppDispatch, RootState } from 'app/store/store';
|
import type { AppDispatch, RootState } from 'app/store/store';
|
||||||
|
|
||||||
|
import { addSetDefaultSettingsListener } from './listeners/setDefaultSettings';
|
||||||
|
|
||||||
export const listenerMiddleware = createListenerMiddleware();
|
export const listenerMiddleware = createListenerMiddleware();
|
||||||
|
|
||||||
export type AppStartListening = TypedStartListening<RootState, AppDispatch>;
|
export type AppStartListening = TypedStartListening<RootState, AppDispatch>;
|
||||||
@ -153,3 +155,5 @@ addUpscaleRequestedListener(startAppListening);
|
|||||||
|
|
||||||
// Dynamic prompts
|
// Dynamic prompts
|
||||||
addDynamicPromptsListener(startAppListening);
|
addDynamicPromptsListener(startAppListening);
|
||||||
|
|
||||||
|
addSetDefaultSettingsListener(startAppListening);
|
||||||
|
@ -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' }) })));
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
@ -1,4 +1,5 @@
|
|||||||
import type { CONTROLNET_PROCESSORS } from 'features/controlAdapters/store/constants';
|
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 { InvokeTabName } from 'features/ui/store/tabMap';
|
||||||
import type { O } from 'ts-toolbelt';
|
import type { O } from 'ts-toolbelt';
|
||||||
|
|
||||||
@ -82,6 +83,8 @@ export type AppConfig = {
|
|||||||
guidance: NumericalParameterConfig;
|
guidance: NumericalParameterConfig;
|
||||||
cfgRescaleMultiplier: NumericalParameterConfig;
|
cfgRescaleMultiplier: NumericalParameterConfig;
|
||||||
img2imgStrength: NumericalParameterConfig;
|
img2imgStrength: NumericalParameterConfig;
|
||||||
|
scheduler?: ParameterScheduler;
|
||||||
|
vaePrecision?: ParameterPrecision;
|
||||||
// Canvas
|
// Canvas
|
||||||
boundingBoxHeight: NumericalParameterConfig; // initial value comes from model
|
boundingBoxHeight: NumericalParameterConfig; // initial value comes from model
|
||||||
boundingBoxWidth: NumericalParameterConfig; // initial value comes from model
|
boundingBoxWidth: NumericalParameterConfig; // initial value comes from model
|
||||||
|
@ -8,7 +8,7 @@ export const ModelPane = () => {
|
|||||||
const selectedModelKey = useAppSelector((s) => s.modelmanagerV2.selectedModelKey);
|
const selectedModelKey = useAppSelector((s) => s.modelmanagerV2.selectedModelKey);
|
||||||
return (
|
return (
|
||||||
<Box layerStyle="first" p={2} borderRadius="base" w="50%" h="full">
|
<Box layerStyle="first" p={2} borderRadius="base" w="50%" h="full">
|
||||||
{selectedModelKey ? <Model /> : <ImportModels />}
|
{selectedModelKey ? <Model key={selectedModelKey} /> : <ImportModels />}
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -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 <Loading />;
|
||||||
|
}
|
||||||
|
|
||||||
|
return <DefaultSettingsForm defaultSettingsDefaults={defaultSettingsDefaults} />;
|
||||||
|
};
|
@ -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<DefaultSettingsFormData>) {
|
||||||
|
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 (
|
||||||
|
<FormControl flexDir="column" gap={1} alignItems="flex-start">
|
||||||
|
<InformationalPopover feature="paramCFGRescaleMultiplier">
|
||||||
|
<FormLabel>{t('parameters.cfgRescaleMultiplier')}</FormLabel>
|
||||||
|
</InformationalPopover>
|
||||||
|
<Flex w="full" gap={1}>
|
||||||
|
<CompositeSlider
|
||||||
|
value={value}
|
||||||
|
min={sliderMin}
|
||||||
|
max={sliderMax}
|
||||||
|
step={coarseStep}
|
||||||
|
fineStep={fineStep}
|
||||||
|
onChange={onChange}
|
||||||
|
marks={marks}
|
||||||
|
isDisabled={isDisabled}
|
||||||
|
/>
|
||||||
|
<CompositeNumberInput
|
||||||
|
value={value}
|
||||||
|
min={numberInputMin}
|
||||||
|
max={numberInputMax}
|
||||||
|
step={coarseStep}
|
||||||
|
fineStep={fineStep}
|
||||||
|
onChange={onChange}
|
||||||
|
isDisabled={isDisabled}
|
||||||
|
/>
|
||||||
|
</Flex>
|
||||||
|
</FormControl>
|
||||||
|
);
|
||||||
|
}
|
@ -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<DefaultSettingsFormData>) {
|
||||||
|
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 (
|
||||||
|
<FormControl flexDir="column" gap={1} alignItems="flex-start">
|
||||||
|
<InformationalPopover feature="paramCFGScale">
|
||||||
|
<FormLabel>{t('parameters.cfgScale')}</FormLabel>
|
||||||
|
</InformationalPopover>
|
||||||
|
<Flex w="full" gap={1}>
|
||||||
|
<CompositeSlider
|
||||||
|
value={value}
|
||||||
|
min={sliderMin}
|
||||||
|
max={sliderMax}
|
||||||
|
step={coarseStep}
|
||||||
|
fineStep={fineStep}
|
||||||
|
onChange={onChange}
|
||||||
|
marks={marks}
|
||||||
|
isDisabled={isDisabled}
|
||||||
|
/>
|
||||||
|
<CompositeNumberInput
|
||||||
|
value={value}
|
||||||
|
min={numberInputMin}
|
||||||
|
max={numberInputMax}
|
||||||
|
step={coarseStep}
|
||||||
|
fineStep={fineStep}
|
||||||
|
onChange={onChange}
|
||||||
|
isDisabled={isDisabled}
|
||||||
|
/>
|
||||||
|
</Flex>
|
||||||
|
</FormControl>
|
||||||
|
);
|
||||||
|
}
|
@ -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<DefaultSettingsFormData>) {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const { field } = useController(props);
|
||||||
|
|
||||||
|
const onChange = useCallback<ComboboxOnChange>(
|
||||||
|
(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 (
|
||||||
|
<FormControl flexDir="column" gap={1} alignItems="flex-start">
|
||||||
|
<InformationalPopover feature="paramScheduler">
|
||||||
|
<FormLabel>{t('parameters.scheduler')}</FormLabel>
|
||||||
|
</InformationalPopover>
|
||||||
|
<Combobox isDisabled={isDisabled} value={value} options={SCHEDULER_OPTIONS} onChange={onChange} />
|
||||||
|
</FormControl>
|
||||||
|
);
|
||||||
|
}
|
@ -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<T> {
|
||||||
|
value: T;
|
||||||
|
isEnabled: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type DefaultSettingsFormData = {
|
||||||
|
vae: FormField<string>;
|
||||||
|
vaePrecision: FormField<string>;
|
||||||
|
scheduler: FormField<ParameterScheduler>;
|
||||||
|
steps: FormField<number>;
|
||||||
|
cfgScale: FormField<number>;
|
||||||
|
cfgRescaleMultiplier: FormField<number>;
|
||||||
|
};
|
||||||
|
|
||||||
|
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<DefaultSettingsFormData>({
|
||||||
|
defaultValues: defaultSettingsDefaults,
|
||||||
|
});
|
||||||
|
|
||||||
|
const onSubmit = useCallback<SubmitHandler<DefaultSettingsFormData>>(
|
||||||
|
(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 (
|
||||||
|
<>
|
||||||
|
<Flex gap="2" justifyContent="space-between" w="full" mb={5}>
|
||||||
|
<Heading fontSize="md">{t('modelManager.defaultSettings')}</Heading>
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
leftIcon={<IoPencil />}
|
||||||
|
colorScheme="invokeYellow"
|
||||||
|
isDisabled={!formState.isDirty}
|
||||||
|
onClick={handleSubmit(onSubmit)}
|
||||||
|
type="submit"
|
||||||
|
isLoading={isLoading}
|
||||||
|
>
|
||||||
|
{t('common.save')}
|
||||||
|
</Button>
|
||||||
|
</Flex>
|
||||||
|
|
||||||
|
<Flex flexDir="column" gap={8}>
|
||||||
|
<Flex gap={8}>
|
||||||
|
<Flex gap={4} w="full">
|
||||||
|
<SettingToggle control={control} name="vae" />
|
||||||
|
<DefaultVae control={control} name="vae" />
|
||||||
|
</Flex>
|
||||||
|
<Flex gap={4} w="full">
|
||||||
|
<SettingToggle control={control} name="vaePrecision" />
|
||||||
|
<DefaultVaePrecision control={control} name="vaePrecision" />
|
||||||
|
</Flex>
|
||||||
|
</Flex>
|
||||||
|
<Flex gap={8}>
|
||||||
|
<Flex gap={4} w="full">
|
||||||
|
<SettingToggle control={control} name="scheduler" />
|
||||||
|
<DefaultScheduler control={control} name="scheduler" />
|
||||||
|
</Flex>
|
||||||
|
<Flex gap={4} w="full">
|
||||||
|
<SettingToggle control={control} name="steps" />
|
||||||
|
<DefaultSteps control={control} name="steps" />
|
||||||
|
</Flex>
|
||||||
|
</Flex>
|
||||||
|
<Flex gap={8}>
|
||||||
|
<Flex gap={4} w="full">
|
||||||
|
<SettingToggle control={control} name="cfgScale" />
|
||||||
|
<DefaultCfgScale control={control} name="cfgScale" />
|
||||||
|
</Flex>
|
||||||
|
<Flex gap={4} w="full">
|
||||||
|
<SettingToggle control={control} name="cfgRescaleMultiplier" />
|
||||||
|
<DefaultCfgRescaleMultiplier control={control} name="cfgRescaleMultiplier" />
|
||||||
|
</Flex>
|
||||||
|
</Flex>
|
||||||
|
</Flex>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
@ -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<DefaultSettingsFormData>) {
|
||||||
|
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 (
|
||||||
|
<FormControl flexDir="column" gap={1} alignItems="flex-start">
|
||||||
|
<InformationalPopover feature="paramSteps">
|
||||||
|
<FormLabel>{t('parameters.steps')}</FormLabel>
|
||||||
|
</InformationalPopover>
|
||||||
|
<Flex w="full" gap={1}>
|
||||||
|
<CompositeSlider
|
||||||
|
value={value}
|
||||||
|
min={sliderMin}
|
||||||
|
max={sliderMax}
|
||||||
|
step={coarseStep}
|
||||||
|
fineStep={fineStep}
|
||||||
|
onChange={onChange}
|
||||||
|
marks={marks}
|
||||||
|
isDisabled={isDisabled}
|
||||||
|
/>
|
||||||
|
<CompositeNumberInput
|
||||||
|
value={value}
|
||||||
|
min={numberInputMin}
|
||||||
|
max={numberInputMax}
|
||||||
|
step={coarseStep}
|
||||||
|
fineStep={fineStep}
|
||||||
|
onChange={onChange}
|
||||||
|
isDisabled={isDisabled}
|
||||||
|
/>
|
||||||
|
</Flex>
|
||||||
|
</FormControl>
|
||||||
|
);
|
||||||
|
}
|
@ -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<DefaultSettingsFormData>) {
|
||||||
|
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<ComboboxOnChange>(
|
||||||
|
(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 (
|
||||||
|
<FormControl flexDir="column" gap={1} alignItems="flex-start">
|
||||||
|
<InformationalPopover feature="paramVAE">
|
||||||
|
<FormLabel>{t('modelManager.vae')}</FormLabel>
|
||||||
|
</InformationalPopover>
|
||||||
|
<Combobox isDisabled={isDisabled} value={value} options={compatibleOptions} onChange={onChange} />
|
||||||
|
</FormControl>
|
||||||
|
);
|
||||||
|
}
|
@ -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<DefaultSettingsFormData>) {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const { field } = useController(props);
|
||||||
|
|
||||||
|
const onChange = useCallback<ComboboxOnChange>(
|
||||||
|
(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 (
|
||||||
|
<FormControl flexDir="column" gap={1} alignItems="flex-start">
|
||||||
|
<InformationalPopover feature="paramVAEPrecision">
|
||||||
|
<FormLabel>{t('modelManager.vaePrecision')}</FormLabel>
|
||||||
|
</InformationalPopover>
|
||||||
|
<Combobox isDisabled={isDisabled} value={value} options={options} onChange={onChange} />
|
||||||
|
</FormControl>
|
||||||
|
);
|
||||||
|
}
|
@ -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<T>(props: UseControllerProps<DefaultSettingsFormData>) {
|
||||||
|
const { field } = useController(props);
|
||||||
|
|
||||||
|
const value = useMemo(() => {
|
||||||
|
return !!(field.value as FormField<T>).isEnabled;
|
||||||
|
}, [field.value]);
|
||||||
|
|
||||||
|
const onChange = useCallback(
|
||||||
|
(e: ChangeEvent<HTMLInputElement>) => {
|
||||||
|
const updatedValue: FormField<T> = {
|
||||||
|
...(field.value as FormField<T>),
|
||||||
|
isEnabled: e.target.checked,
|
||||||
|
};
|
||||||
|
field.onChange(updatedValue);
|
||||||
|
},
|
||||||
|
[field]
|
||||||
|
);
|
||||||
|
|
||||||
|
return <Switch isChecked={value} onChange={onChange} />;
|
||||||
|
}
|
@ -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 (
|
||||||
|
<>
|
||||||
|
<Flex flexDir="column" height="full" gap="3">
|
||||||
|
<DataViewer label="metadata" data={metadata || {}} />
|
||||||
|
</Flex>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
@ -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 { 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 { ModelEdit } from './ModelEdit';
|
||||||
import { ModelView } from './ModelView';
|
import { ModelView } from './ModelView';
|
||||||
|
|
||||||
export const Model = () => {
|
export const Model = () => {
|
||||||
|
const { t } = useTranslation();
|
||||||
const selectedModelMode = useAppSelector((s) => s.modelmanagerV2.selectedModelMode);
|
const selectedModelMode = useAppSelector((s) => s.modelmanagerV2.selectedModelMode);
|
||||||
return selectedModelMode === 'view' ? <ModelView /> : <ModelEdit />;
|
const selectedModelKey = useAppSelector((s) => s.modelmanagerV2.selectedModelKey);
|
||||||
|
const { data, isLoading } = useGetModelConfigQuery(selectedModelKey ?? skipToken);
|
||||||
|
|
||||||
|
if (isLoading) {
|
||||||
|
return <Text>{t('common.loading')}</Text>;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!data) {
|
||||||
|
return <Text>{t('common.somethingWentWrong')}</Text>;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Flex flexDir="column" gap={1} p={2}>
|
||||||
|
<Heading as="h2" fontSize="lg">
|
||||||
|
{data.name}
|
||||||
|
</Heading>
|
||||||
|
|
||||||
|
{data.source && (
|
||||||
|
<Text variant="subtext">
|
||||||
|
{t('modelManager.source')}: {data?.source}
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
|
<Box mt="4">
|
||||||
|
<ModelAttrView label="Description" value={data.description} />
|
||||||
|
</Box>
|
||||||
|
</Flex>
|
||||||
|
|
||||||
|
<Tabs mt="4" h="100%">
|
||||||
|
<TabList>
|
||||||
|
<Tab>{t('modelManager.settings')}</Tab>
|
||||||
|
<Tab>{t('modelManager.metadata')}</Tab>
|
||||||
|
</TabList>
|
||||||
|
|
||||||
|
<TabPanels h="100%">
|
||||||
|
<TabPanel>{selectedModelMode === 'view' ? <ModelView /> : <ModelEdit />}</TabPanel>
|
||||||
|
<TabPanel h="full">
|
||||||
|
<ModelMetadata />
|
||||||
|
</TabPanel>
|
||||||
|
</TabPanels>
|
||||||
|
</Tabs>
|
||||||
|
</>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
@ -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 { skipToken } from '@reduxjs/toolkit/query';
|
||||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||||
import DataViewer from 'features/gallery/components/ImageMetadataViewer/DataViewer';
|
|
||||||
import { setSelectedModelMode } from 'features/modelManagerV2/store/modelManagerV2Slice';
|
import { setSelectedModelMode } from 'features/modelManagerV2/store/modelManagerV2Slice';
|
||||||
import { useCallback, useMemo } from 'react';
|
import { useCallback, useMemo } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { IoPencil } from 'react-icons/io5';
|
import { IoPencil } from 'react-icons/io5';
|
||||||
import { useGetModelConfigQuery, useGetModelMetadataQuery } from 'services/api/endpoints/models';
|
import { useGetModelConfigQuery } from 'services/api/endpoints/models';
|
||||||
import type {
|
import type {
|
||||||
CheckpointModelConfig,
|
CheckpointModelConfig,
|
||||||
ControlNetModelConfig,
|
ControlNetModelConfig,
|
||||||
@ -18,6 +17,7 @@ import type {
|
|||||||
VAEModelConfig,
|
VAEModelConfig,
|
||||||
} from 'services/api/types';
|
} from 'services/api/types';
|
||||||
|
|
||||||
|
import { DefaultSettings } from './DefaultSettings';
|
||||||
import { ModelAttrView } from './ModelAttrView';
|
import { ModelAttrView } from './ModelAttrView';
|
||||||
import { ModelConvert } from './ModelConvert';
|
import { ModelConvert } from './ModelConvert';
|
||||||
|
|
||||||
@ -26,7 +26,6 @@ export const ModelView = () => {
|
|||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
const selectedModelKey = useAppSelector((s) => s.modelmanagerV2.selectedModelKey);
|
const selectedModelKey = useAppSelector((s) => s.modelmanagerV2.selectedModelKey);
|
||||||
const { data, isLoading } = useGetModelConfigQuery(selectedModelKey ?? skipToken);
|
const { data, isLoading } = useGetModelConfigQuery(selectedModelKey ?? skipToken);
|
||||||
const { data: metadata } = useGetModelMetadataQuery(selectedModelKey ?? skipToken);
|
|
||||||
|
|
||||||
const modelData = useMemo(() => {
|
const modelData = useMemo(() => {
|
||||||
if (!data) {
|
if (!data) {
|
||||||
@ -73,35 +72,15 @@ export const ModelView = () => {
|
|||||||
return <Text>{t('common.somethingWentWrong')}</Text>;
|
return <Text>{t('common.somethingWentWrong')}</Text>;
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<Flex flexDir="column" h="full">
|
<Flex flexDir="column" h="full" gap="2">
|
||||||
<Flex w="full" justifyContent="space-between">
|
<Box layerStyle="second" borderRadius="base" p={3}>
|
||||||
<Flex flexDir="column" gap={1} p={2}>
|
<Flex gap="2" justifyContent="flex-end" w="full">
|
||||||
<Heading as="h2" fontSize="lg">
|
|
||||||
{modelData.name}
|
|
||||||
</Heading>
|
|
||||||
|
|
||||||
{modelData.source && (
|
|
||||||
<Text variant="subtext">
|
|
||||||
{t('modelManager.source')}: {modelData.source}
|
|
||||||
</Text>
|
|
||||||
)}
|
|
||||||
</Flex>
|
|
||||||
<Flex gap={2}>
|
|
||||||
<Button size="sm" leftIcon={<IoPencil />} colorScheme="invokeYellow" onClick={handleEditModel}>
|
<Button size="sm" leftIcon={<IoPencil />} colorScheme="invokeYellow" onClick={handleEditModel}>
|
||||||
{t('modelManager.edit')}
|
{t('modelManager.edit')}
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
{modelData.type === 'main' && modelData.format === 'checkpoint' && <ModelConvert model={modelData} />}
|
{modelData.type === 'main' && modelData.format === 'checkpoint' && <ModelConvert model={modelData} />}
|
||||||
</Flex>
|
</Flex>
|
||||||
</Flex>
|
|
||||||
|
|
||||||
<Flex flexDir="column" p={2} gap={3}>
|
|
||||||
<Flex>
|
|
||||||
<ModelAttrView label="Description" value={modelData.description} />
|
|
||||||
</Flex>
|
|
||||||
<Heading as="h3" fontSize="md" mt="4">
|
|
||||||
{t('modelManager.modelSettings')}
|
|
||||||
</Heading>
|
|
||||||
<Box layerStyle="second" borderRadius="base" p={3}>
|
|
||||||
<Flex flexDir="column" gap={3}>
|
<Flex flexDir="column" gap={3}>
|
||||||
<Flex gap={2}>
|
<Flex gap={2}>
|
||||||
<ModelAttrView label={t('modelManager.baseModel')} value={modelData.base} />
|
<ModelAttrView label={t('modelManager.baseModel')} value={modelData.base} />
|
||||||
@ -140,18 +119,9 @@ export const ModelView = () => {
|
|||||||
)}
|
)}
|
||||||
</Flex>
|
</Flex>
|
||||||
</Box>
|
</Box>
|
||||||
</Flex>
|
<Box layerStyle="second" borderRadius="base" p={3}>
|
||||||
|
<DefaultSettings />
|
||||||
{metadata && (
|
</Box>
|
||||||
<>
|
|
||||||
<Heading as="h3" fontSize="md" mt="4">
|
|
||||||
{t('modelManager.modelMetadata')}
|
|
||||||
</Heading>
|
|
||||||
<Flex h="full" w="full" p={2}>
|
|
||||||
<DataViewer label="metadata" data={metadata} />
|
|
||||||
</Flex>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</Flex>
|
</Flex>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -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 (
|
||||||
|
<IconButton
|
||||||
|
icon={<RiSparklingFill />}
|
||||||
|
tooltip={t('modelManager.useDefaultSettings')}
|
||||||
|
aria-label={t('modelManager.useDefaultSettings')}
|
||||||
|
isDisabled={!model}
|
||||||
|
onClick={handleClickDefaultSettings}
|
||||||
|
size="sm"
|
||||||
|
variant="ghost"
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
@ -5,3 +5,5 @@ import type { ImageDTO } from 'services/api/types';
|
|||||||
export const initialImageSelected = createAction<ImageDTO | undefined>('generation/initialImageSelected');
|
export const initialImageSelected = createAction<ImageDTO | undefined>('generation/initialImageSelected');
|
||||||
|
|
||||||
export const modelSelected = createAction<ParameterModel>('generation/modelSelected');
|
export const modelSelected = createAction<ParameterModel>('generation/modelSelected');
|
||||||
|
|
||||||
|
export const setDefaultSettings = createAction('generation/setDefaultSettings');
|
||||||
|
@ -230,6 +230,12 @@ export const generationSlice = createSlice({
|
|||||||
state.height = optimalDimension;
|
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
|
// TODO: This is a temp fix to reduce issues with T2I adapter having a different downscaling
|
||||||
|
@ -12,6 +12,7 @@ import ParamScheduler from 'features/parameters/components/Core/ParamScheduler';
|
|||||||
import ParamSteps from 'features/parameters/components/Core/ParamSteps';
|
import ParamSteps from 'features/parameters/components/Core/ParamSteps';
|
||||||
import { NavigateToModelManagerButton } from 'features/parameters/components/MainModel/NavigateToModelManagerButton';
|
import { NavigateToModelManagerButton } from 'features/parameters/components/MainModel/NavigateToModelManagerButton';
|
||||||
import ParamMainModelSelect from 'features/parameters/components/MainModel/ParamMainModelSelect';
|
import ParamMainModelSelect from 'features/parameters/components/MainModel/ParamMainModelSelect';
|
||||||
|
import { UseDefaultSettingsButton } from 'features/parameters/components/MainModel/UseDefaultSettingsButton';
|
||||||
import { useExpanderToggle } from 'features/settingsAccordions/hooks/useExpanderToggle';
|
import { useExpanderToggle } from 'features/settingsAccordions/hooks/useExpanderToggle';
|
||||||
import { useStandaloneAccordionToggle } from 'features/settingsAccordions/hooks/useStandaloneAccordionToggle';
|
import { useStandaloneAccordionToggle } from 'features/settingsAccordions/hooks/useStandaloneAccordionToggle';
|
||||||
import { filter } from 'lodash-es';
|
import { filter } from 'lodash-es';
|
||||||
@ -58,6 +59,7 @@ export const GenerationSettingsAccordion = memo(() => {
|
|||||||
<Flex gap={4} alignItems="center">
|
<Flex gap={4} alignItems="center">
|
||||||
<ParamMainModelSelect />
|
<ParamMainModelSelect />
|
||||||
<Flex>
|
<Flex>
|
||||||
|
<UseDefaultSettingsButton />
|
||||||
<SyncModelsIconButton />
|
<SyncModelsIconButton />
|
||||||
<NavigateToModelManagerButton />
|
<NavigateToModelManagerButton />
|
||||||
</Flex>
|
</Flex>
|
||||||
|
@ -41,6 +41,8 @@ const initialConfigState: AppConfig = {
|
|||||||
boundingBoxHeight: { ...baseDimensionConfig },
|
boundingBoxHeight: { ...baseDimensionConfig },
|
||||||
scaledBoundingBoxWidth: { ...baseDimensionConfig },
|
scaledBoundingBoxWidth: { ...baseDimensionConfig },
|
||||||
scaledBoundingBoxHeight: { ...baseDimensionConfig },
|
scaledBoundingBoxHeight: { ...baseDimensionConfig },
|
||||||
|
scheduler: 'euler',
|
||||||
|
vaePrecision: 'fp32',
|
||||||
steps: {
|
steps: {
|
||||||
initial: 30,
|
initial: 30,
|
||||||
sliderMin: 1,
|
sliderMin: 1,
|
||||||
|
@ -24,7 +24,15 @@ export type UpdateModelArg = {
|
|||||||
body: paths['/api/v2/models/i/{key}']['patch']['requestBody']['content']['application/json'];
|
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 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 GetModelConfigResponse = paths['/api/v2/models/i/{key}']['get']['responses']['200']['content']['application/json'];
|
||||||
|
|
||||||
type GetModelMetadataResponse =
|
type GetModelMetadataResponse =
|
||||||
@ -172,6 +180,16 @@ export const modelsApi = api.injectEndpoints({
|
|||||||
},
|
},
|
||||||
invalidatesTags: ['Model'],
|
invalidatesTags: ['Model'],
|
||||||
}),
|
}),
|
||||||
|
updateModelMetadata: build.mutation<UpdateModelMetadataResponse, UpdateModelMetadataArg>({
|
||||||
|
query: ({ key, body }) => {
|
||||||
|
return {
|
||||||
|
url: buildModelsUrl(`i/${key}/metadata`),
|
||||||
|
method: 'PATCH',
|
||||||
|
body: body,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
invalidatesTags: ['Model'],
|
||||||
|
}),
|
||||||
installModel: build.mutation<InstallModelResponse, InstallModelArg>({
|
installModel: build.mutation<InstallModelResponse, InstallModelArg>({
|
||||||
query: ({ source, config, access_token }) => {
|
query: ({ source, config, access_token }) => {
|
||||||
return {
|
return {
|
||||||
@ -351,6 +369,7 @@ export const {
|
|||||||
useGetModelMetadataQuery,
|
useGetModelMetadataQuery,
|
||||||
useDeleteModelImportMutation,
|
useDeleteModelImportMutation,
|
||||||
usePruneModelImportsMutation,
|
usePruneModelImportsMutation,
|
||||||
|
useUpdateModelMetadataMutation,
|
||||||
} = modelsApi;
|
} = modelsApi;
|
||||||
|
|
||||||
const upsertModelConfigs = (
|
const upsertModelConfigs = (
|
||||||
|
File diff suppressed because one or more lines are too long
Loading…
Reference in New Issue
Block a user